在TypeScript Cookbook中,Stefan Baumgartner 巧妙地涵盖了从项目设置到高级类型技术的所有内容,提供了丰富的实际示例和宝贵的见解,让您成为随时准备应对任何挑战的 TypeScript 专家。
Addy Osmani Google Chrome 开发者体验主管
In TypeScript Cookbook, Stefan Baumgartner deftly covers everything from project setup to advanced typing techniques, providing a wealth of practical examples and valuable insights to make you a TypeScript expert ready for any challenge.
Addy Osmani Head of Chrome Developer Experience, Google
Typescript Cookbook是想要学习如何有效使用 TypeScript 的开发人员的必备资源。Stefan 将解决实际问题的简洁明了的方案打包成一本全面的手册,可帮助您从新手晋升为专家。
Simona Cotin Google Angular 工程经理
Typescript Cookbook is an essential resource for developers who want to learn how to use TypeScript effectively. Stefan packs clear and concise recipes for solving real-world problems into a comprehensive playbook that upskills you from novice to expert.
Simona Cotin Engineering Manager for Angular, Google
TypeScript Cookbook向您展示了如何使用高级类型解决各种问题。更妙的是,它还教您如何使用 TypeScript 的功能为自己编写新类型。
Nathan Shively-Sanders TypeScript 团队软件工程师
TypeScript Cookbook shows you how to solve all sorts of problems with advanced types. Even better, it teaches you how to use TypeScript’s features to write new types for yourself.
Nathan Shively-Sanders Software Engineer on the TypeScript team
对于任何使用 TypeScript 的人来说, 《TypeScript Cookbook》都是一本非常有价值的参考书。它将大量有价值的信息浓缩成一种您可以轻松查阅的格式。
Total TypeScript的作者Matt Pocock
TypeScript Cookbook is an extremely valuable reference for anyone working with TypeScript. It condenses a ton of valuable information into a format you can dip into and out of easily.
Matt Pocock Author of Total TypeScript
TypeScript 有时会减慢开发人员的速度,但TypeScript Cookbook是完美的解决方案!针对常见 TypeScript 问题提供的全面解决方案使其成为提高生产力不可或缺的工具。
Vanessa Böhner Zavvy 首席前端开发人员
TypeScript can sometimes slow developers down, but TypeScript Cookbook is the perfect remedy! The comprehensive solutions offered for common TypeScript problems make it an indispensable tool for improving productivity.
Vanessa Böhner Lead Front-End Developer, Zavvy
TypeScript Cookbook是一本值得一读的书,信息量丰富。我非常喜欢简洁的问题和答案,以及对它们背后细微差别的精心讨论。我从每一章中学到了很多巧妙的技巧和时髦的新模式。任何 TypeScript 开发人员都应该学习这些细微差别、技巧和模式——尤其是从这本书中。强烈推荐!
Josh Goldberg 《Learning TypeScript》的作者
TypeScript Cookbook is a lovely read and a fount of information. I thoroughly enjoyed the succinct questions and answers followed by well-crafted discussions of the nuances behind them. I learned a ton of neat tricks and snazzy new patterns from each of the chapters. It would behoove any TypeScript developer to learn those nuances, tricks, and patterns—in particular from this book. Would highly recommend!
Josh Goldberg Author of Learning TypeScript
我意识到自己在编写 TypeScript 时遇到了很多问题,Stefan给出的建议清晰、准确且富有洞察力。有了这份参考资料,我对 TypeScript 更有信心了。
Phil Nash,Sonar 开发倡导者
I recognized so many issues that I’d come across in my own TypeScript and found Stefan’s advice on them to be clear, precise, and insightful. I feel more confident with TypeScript with this reference by my side.
Phil Nash, Developer Advocate, Sonar
真实世界类型级编程
Real World Type-Level Programming
版权所有 © 2023 Stefan Baumgartner。保留所有权利。
Copyright © 2023 Stefan Baumgartner. All rights reserved.
在美国印刷。
Printed in the United States of America.
由O'Reilly Media, Inc. 出版 ,地址为 1005 Gravenstein Highway North, Sebastopol, CA 95472。
Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
O'Reilly 的书籍可用于教育、商业或促销用途。大多数书籍都有在线版本 ( http://oreilly.com )。如需更多信息,请联系我们的企业/机构销售部门:800-998-9938 或corporate@oreilly.com。
O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com.
有关发布详细信息, 请参阅 http://oreilly.com/catalog/errata.csp?isbn=9781098136659 。
See http://oreilly.com/catalog/errata.csp?isbn=9781098136659 for release details.
O'Reilly 徽标是 O'Reilly Media, Inc. 的注册商标。TypeScript Cookbook、封面图片和相关商业外观是 O'Reilly Media, Inc. 的商标。
The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. TypeScript Cookbook, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.
本作品中表达的观点为作者的观点,不代表出版商的观点。尽管出版商和作者已尽最大努力确保本作品中包含的信息和说明准确无误,但出版商和作者对错误或遗漏不承担任何责任,包括但不限于因使用或依赖本作品而造成的损害的责任。使用本作品中包含的信息和说明的风险由您自行承担。如果本作品包含或描述的任何代码示例或其他技术受开源许可或他人的知识产权约束,则您有责任确保您对其的使用符合此类许可和/或权利。
The views expressed in this work are those of the author and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.
978-1-098-13665-9
978-1-098-13665-9
[大规模集成电路]
[LSI]
我总是很高兴见证编程语言的演变及其对软件开发的影响。TypeScript 是 JavaScript 的超集,也不例外。事实上,TypeScript 已迅速崛起成为最广泛使用的编程语言之一,在 Web 开发领域为自己开辟了独特的空间。由于这种语言已经获得了广泛的采用和赞誉,因此在TypeScript Cookbook中对它进行全面的介绍是再合适不过的了。
I am always excited to witness the evolution of programming languages and the impact they make on software development. TypeScript, a superset of JavaScript, is no exception. In fact, TypeScript has swiftly risen to become one of the most widely used programming languages, carving out a unique space for itself in the world of web development. As this language has garnered significant adoption and praise, it is only fitting that it be given the comprehensive treatment it deserves in TypeScript Cookbook.
作为一名狂热的 TypeScript 用户,我必须说,它为 JavaScript 带来的精确性和稳健性既令人振奋又令人惊叹。其背后的一个关键原因是它的类型安全性,这解决了人们对 JavaScript 的长期批评。通过允许开发人员为变量定义严格类型,TypeScript 使得在编译过程中捕获错误变得更加容易,从而显著提高了代码质量和可维护性。
As an avid TypeScript user, I must say that the precision and robustness it has brought to JavaScript have been both empowering and astonishing. One of the key reasons behind this is its type safety, which has addressed a long-standing criticism of JavaScript. By allowing developers to define strict types for variables, TypeScript has made it easier to catch errors during the compilation process, significantly improving code quality and maintainability.
TypeScript Cookbook是一本非常需要的指南。序言正确地确立了 TypeScript 的飙升人气。然而,对 TypeScript 的这种日益增长的兴趣也揭示了开发人员在采用它时面临的挑战。正是在这里,这本书注定会有所作为。
TypeScript Cookbook is a much-needed guide. The preface rightly establishes TypeScript’s skyrocketing popularity. However, this rising interest in TypeScript also brings to light the challenges developers face in adopting it. It is here that this book is set to make a difference.
本书实用性十足,精心设计,旨在解决 TypeScript 用户面临的实际挑战。它融合了 100 多个配方,涵盖了从基础到高级的各种概念。作为开发人员,我们经常发现自己在与类型检查器作斗争,而这本书将成为您的利剑和盾牌。通过深入的解释,您不仅可以学习如何有效地使用 TypeScript,还可以了解概念背后的思维过程。
Drenched in practicality, this book is meticulously designed to address real-world challenges faced by TypeScript users. It is an amalgamation of more than one hundred recipes that deal with a gamut of concepts ranging from basic to advanced. As developers, we often find ourselves fighting the type-checker, and that’s where this book will serve as your sword and shield. With in-depth explanations, you will not only learn how to work with TypeScript efficiently but also understand the thought processes behind the concepts.
TypeScript Cookbook值得称赞的方面之一是它采用的方法,以适应 TypeScript 的快速发展。由于 TypeScript 每年都会定期发布新版本,因此保持最新状态是一项艰巨的任务。这本书出色地专注于 TypeScript 的长期方面,并确保您的学习在不断变化的环境中仍然具有相关性。
One of the many laudable aspects of TypeScript Cookbook is its approach toward embracing TypeScript’s rapid evolution. With TypeScript getting regular releases per year, staying up to date is a Herculean task. This book does a splendid job focusing on the long-lasting aspects of TypeScript and ensures that your learning remains relevant despite the ever-changing landscape.
除了大量的食谱之外,本书还鼓励您理解 JavaScript 和 TypeScript 之间错综复杂的联系。了解这两种语言之间的共生关系对于释放 TypeScript 的真正潜力至关重要。无论您是在处理类型断言、泛型,还是将 TypeScript 与流行的库和框架(如 React)集成,本书都能涵盖所有内容。
In addition to a plethora of recipes, the book encourages you to comprehend the intricate connection between JavaScript and TypeScript. Understanding the symbiotic relationship between these two languages is paramount in unlocking TypeScript’s true potential. Whether you are struggling with type assertions, generics, or even integrating TypeScript with popular libraries and frameworks such as React, this book covers it all.
这本书在作为指南和参考书方面也表现出色。作为指南,它可以无缝地带你从新手变成专家。作为参考书,它可以作为你整个 TypeScript 旅程的可靠伴侣。本书的组织无可挑剔,确保每一章都可以单独阅读,但放在一起时会形成一个有凝聚力的知识库。
This book also excels in serving as both a guide and a reference. As a guide, it seamlessly takes you from novice to expert. As a reference, it serves as a reliable companion throughout your TypeScript journey. The organization of the book is impeccable, ensuring that each chapter can be consumed in isolation, yet forming a cohesive knowledge base when put together.
随着 TypeScript 的流行度没有丝毫减弱的迹象,TypeScript Cookbook有望成为每一位 TypeScript 爱好者的必备资源。从现实世界的例子到解决方案的宝库,这本书是您在激动人心的 TypeScript 世界中导航所需的指南针。
With TypeScript’s popularity showing no signs of slowing down, TypeScript Cookbook is poised to be an essential resource for every TypeScript enthusiast. From real-world examples to a treasure trove of solutions, this book is the compass you need to navigate the exciting world of TypeScript.
无论您是初涉 TypeScript 还是想要深入了解 TypeScript,这本书都是知识的灯塔。我衷心祝贺 Stefan Baumgartner 创作了这部杰作,并欢迎大家品尝 TypeScript 的成功秘诀。
Whether you are getting your feet wet or looking to dive into the depths of TypeScript, this book is a beacon of knowledge. I extend my heartfelt congratulations to Stefan Baumgartner for crafting this masterpiece and welcome you all to savor the recipes of success in TypeScript.
让我们开始 TypeScript 之旅。
Let the journey into TypeScript begin.
您阅读这句话的唯一方式是打开这本书,无论是实体书还是数字书。这说明您对 TypeScript 感兴趣,它是近年来最流行的编程语言之一。根据2022 年 JavaScript 状态调查,几乎 70% 的参与者都在积极使用 TypeScript。2022年 StackOverflow 调查将 TypeScript 列为五大最受欢迎的语言之一,用户满意度排名第四。截至 2023 年初,TypeScript在 NPM 上的每周下载量超过 4000 万次。
The only way you can read this sentence is by opening this book, either physically or digitally. This tells me you are interested in TypeScript, one of the most popular programming languages in recent years. According to the 2022 State of JavaScript survey, almost 70% of all participants actively use TypeScript. The 2022 StackOverflow survey lists TypeScript as one of the top five most popular languages and the fourth highest in user satisfaction. At the beginning of 2023, TypeScript counts more than 40 million weekly downloads on NPM.
毫无疑问:TypeScript 是一种现象!
Without a doubt: TypeScript is a phenomenon!
尽管 TypeScript 很受欢迎,但它仍然让很多开发人员感到为难。与类型检查器作斗争是你经常听到的一句话;另一个说法是,在里面加几个any's 让它闭嘴。有些人觉得自己的速度变慢了,当他们知道他们的代码必须工作时,他们只是为了取悦编译器而编写代码。然而,TypeScript 的唯一目的是提高 JavaScript 开发人员的生产力和效率。这个工具最终是否未能实现其目标,或者我们作为开发人员对该工具的期望是否与它设计交付的不同?
Despite its popularity, TypeScript still gives a lot of developers a hard time. Fighting the type-checker is one phrase you hear often; another one is throwing a couple of any’s in there so it shuts up. Some people feel slowed down, writing just to please the compiler when they know their code has to work. However, TypeScript’s sole purpose is to make JavaScript developers more productive and efficient. Does the tool ultimately fail to meet its goals, or do we as developers expect something different from the tool than it is designed to deliver?
答案介于两者之间,这就是TypeScript Cookbook 的作用所在。在这本书中,您将找到一百多个配方,涵盖从复杂的项目设置到高级类型技术等所有方面。您将了解类型系统的复杂性和内部工作原理,以及它必须做出的权衡和例外,以免干扰其基础:JavaScript。您还将学习方法、设计模式和开发技术,以创建更好、更强大的 TypeScript 代码。最后,您不仅会了解如何做某事,还会了解为什么做某事。
The answer is somewhere in the middle, and this is where TypeScript Cookbook comes in. In this book, you will find more than one hundred recipes that deal with everything from complex project setups to advanced typing techniques. You will learn about the intricacies and inner workings of the type system, as well as the trade-offs and exceptions it has to make to not interfere with its foundation: JavaScript. You also will learn methodologies, design patterns, and development techniques to create better and more robust TypeScript code. In the end, you will understand not only how to do something but also why.
我的目标是为您提供一本指南,带您从新手变成专家,以及一本读完本书后可以快速使用的参考书。由于 TypeScript 每年发布四次,因此不可能在一本书中列出所有最新功能。这就是为什么我们专注于编程语言的长期方面,为您做好迎接所有即将到来的变化的准备。欢迎阅读 TypeScript 手册。
My goal is to give you a guide that takes you from novice to expert, as well as a quick reference you can use well after you’ve read the book. With TypeScript’s four releases per year, it’s impossible to list all the most up-to-date features in a single book. This is why we focus on long-lasting aspects of the programming language, to prepare you for all the changes to come. Welcome to the TypeScript cookbook.
本书面向对 JavaScript 了如指掌且已涉足 TypeScript 的开发人员、工程师和架构师。您了解类型的基本概念及其应用方法,并且了解静态类型的直接优势。现在,事情变得有趣了:您需要对类型系统有更深入的了解,并且需要积极使用 TypeScript,这不仅是为了确保应用程序的稳健性和可扩展性,也是为了保证您与同事之间的协作。
This book is for developers, engineers, and architects who know enough JavaScript to be dangerous and have gotten their feet wet in TypeScript. You understand the fundamental concepts of types and how to apply them, and you understand the immediate benefits of static types. You are at a point where things get interesting: you need a deeper knowledge of the type system, and you need to actively work with TypeScript not only to ensure a robust and scaleable application but also to guarantee collaboration between you and your colleagues.
您想了解 TypeScript 中某些内容的行为方式,以及其行为背后的原因。这就是您在TypeScript Cookbook中可以得到的。您将学习项目设置、类型系统的怪癖和行为;复杂类型及其用例;以及如何使用框架和应用类型开发方法。本书旨在带您从新手到学徒,最终成为专家。如果您需要一本指南来积极学习更多 TypeScript 的复杂功能,同时也需要一本在整个职业生涯中都可以依赖的参考书,那么这本书将是您的最佳选择。
You want to learn about how something behaves in TypeScript, as well as understand the reasoning behind its behavior. This is what you get in TypeScript Cookbook. You will learn project setup, quirks, and behavior of the type system; complex types and their use cases; and working with frameworks and applying type development methodology. This book is designed to take you from novice to apprentice, and eventually to expert. If you need a guide to actively learn more of TypeScript’s sophisticated features, but also a reference you can rely on throughout your career, this book will do right by you.
编写TypeScript Cookbook的主要目标是专注于解决日常问题。TypeScript 是一种出色的编程语言,其类型系统功能非常强大,以至于人们开始用高级TypeScript 谜题来挑战自己。虽然这些脑筋急转弯很有趣,但它们往往缺乏现实世界的背景,因此不属于本书的一部分。
A predominant goal of writing TypeScript Cookbook was to focus on solutions for everyday problems. TypeScript is a remarkable programming language, and the features of the type system are so powerful that we reach a point where people challenge themselves with advanced TypeScript puzzles. While these brain teasers are entertaining, they often lack real-world context and thus are not part of this book.
我希望确保本书介绍的内容是您作为 TypeScript 开发人员在日常生活中会遇到的内容,这些问题源自现实情况,解决方案是全面的。我将教您可在多种场景中使用的技术和方法,而不仅仅是在单一配方中使用。在整本书中,您将找到对早期配方的引用,向您展示如何在新的环境中应用特定技术。
I want to make sure that the content presented is something you will encounter in your day-to-day life as a TypeScript developer, with problems that stem from real-world situations and solutions that are holistic. I will teach you techniques and methodologies you can use in multiple scenarios, not just in a single recipe. Throughout the book you will find references to earlier recipes, showing you how a specific techique can be applied in a new context.
这些示例要么直接从真实项目的源代码中提取,要么精简到基本内容以说明概念,而不需要太多领域知识。虽然有些示例非常具体,但您还会看到很多Person名为“Stefan”的对象(您将在整本书中看到我的年龄)。
The examples are either ripped directly from the source code of real projects or stripped down to essentials to illustrate a concept without requiring too much domain knowledge. While some examples are very specific, you will also see a lot of Person objects that have the name “Stefan” (and you will be able to see me age throughout the book).
本书几乎只关注 TypeScript 在 JavaScript 之上添加的功能;因此,要完全理解示例,您需要了解相当数量的 JavaScript。我不指望您是 JavaScript 专家,但能够阅读基本的 JavaScript 代码是必须的。由于 JavaScript 和 TypeScript 有着如此紧密的 关系,本书中的一些章节讨论了 JavaScript 功能及其行为,但始终通过 TypeScript 的视角。
The book will focus almost exclusively on the features TypeScript adds on top of JavaScript; thus, to understand the example fully, you need to understand a reasonable amount of JavaScript. I don’t expect you to be a JavaScript guru but being able to read basic JavaScript code is a must. Since JavaScript and TypeScript have this strong relationship, some chapters in the book discuss JavaScript features and their behavior, but always through the lens of TypeScript.
烹饪书旨在为您提供问题的快速解决方案:菜谱。在这本书中,每个菜谱都以讨论结束,为您提供解决方案的更广泛的背景和含义。根据作者的风格,O'Reilly 烹饪书的重点要么在于解决方案,要么在于讨论。TypeScript Cookbook无疑是一本讨论书。在我近 20 年的软件编写生涯中,我从未遇到过一种解决方案适合所有问题的情况。这就是为什么我想详细向您展示我们如何得出结论、它们的含义以及权衡利弊。最终,这本书应该成为此类讨论的指南。当您对自己的 决定有适当的论据时,为什么还要做出有根据的猜测?
A cookbook is designed to give you a quick solution to a problem: a recipe. In this book, every recipe ends with a discussion, giving you broader context and meaning for the solution. Depending on the style of the author, the focus of O’Reilly’s cookbooks lies either on the solution or on the discussion. TypeScript Cookbook is unmistakably a discussion book. In my almost 20-year career as a person who writes software, I’ve never encountered situations in which one solution fits all problems. That’s why I want to show you in detail how we came to our conclusions, their meaning, and the trade-offs. Ultimately, this book should be a guide for discussions like that. Why make an educated guess when you have proper arguments for your decisions?
TypeScript Cookbook带您从头到尾了解该语言。我们从项目设置开始,讨论基本类型和类型系统的内部工作原理,最终进入条件类型和辅助类型等高级领域。我们接下来的章节探讨非常具体的功能,例如类的二元性和对 React 的支持,最后学习如何最好地进行类型 开发。
TypeScript Cookbook takes you through the language from start to finish. We start with project setup, talk about basic types and the inner workings of the type system, and ultimately go into advanced territory like conditional types and helper types. We continue with chapters that explore very specific features, like the duality of classes and support for React, and end with learnings on how to best approach type development.
虽然有线索和积累,但每个章节和每个食谱都可以单独阅读。每节课都旨在指出与书中上一个(或下一个!)食谱的联系,但每个章节最终都是独立的。您可以从头到尾阅读,也可以使用“选择自己的冒险”方法,因为它有很多参考资料。以下是内容的简要概述。
While there is a thread and buildup, each chapter and each recipe can be consumed on its own. Each lesson has been designed to point out the connection to previous (or next!) recipes in the book, but each chapter is ultimately self-contained. Feel free to consume it from start to finish, or use the “choose your own adventure” approach with its many references. Here is a brief overview of the content.
TypeScript 想要与所有类型的 JavaScript 兼容,而 JavaScript 有很多不同的类型。在第 1 章“项目设置”中,您将了解不同语言运行时、模块系统和目标平台的配置可能性。
TypeScript wants to work with all flavors of JavaScript, and there are a lot of different flavors. In Chapter 1, “Project Setup” you will learn about configuration possibilities for different language runtimes, module systems, and target platforms.
第 2 章“基本类型”any将引导您了解类型层次结构,告诉您和之间的区别unknown,教您哪些代码对哪个命名空间有贡献,并回答是否选择类型别名或接口来描述对象类型的古老问题。
Chapter 2, “Basic Types” guides you through the type hierarchy, tells you the difference between any and unknown, teaches you which code contributes to which namespace, and answers the age-old question of whether to choose a type alias or an interface to describe your object types.
本书较长的章节之一是第 3 章“类型系统”。在这里,您将学习有关联合类型和交集类型的所有内容,如何定义可区分联合类型,如何使用assert never和optional never技术,以及如何根据您的用例缩小和扩大类型。读完本章后,您将了解为什么 TypeScript 有类型断言而没有类型转换,为什么枚举通常不受欢迎,以及如何在结构类型系统中找到名义位。
One of the longer chapters in the book is Chapter 3, “The Type System”. Here you will learn everything about union and intersection types, how to define discriminated union types, how to use the assert never and optional never techniques, and how to narrow and widen types based on your use case. After this chapter, you will understand why TypeScript has type assertions and no type casts, why enums are generally frowned upon, and how you find the nominal bits in a structural type system.
TypeScript 有一个泛型类型系统,我们将在第 4 章“泛型”中详细介绍。泛型不仅使您的代码更具可重用性,而且也是 TypeScript 更高级功能的入口。本章标志着您从 TypeScript 基础上升到类型系统更复杂领域的起点,这是第一部分的完美结尾。
TypeScript has a generic type system, which we will see in detail in Chapter 4, “Generics”. Generics not only make your code more reusable but are also the entrance to the more advanced features of TypeScript. This chapter marks the point where you ascend from TypeScript basics to the more sophisticated areas of the type system, a fitting end to the first part.
第 5 章“条件类型”解释了为什么 TypeScript 类型系统也是一门元编程语言。有了根据特定条件选择类型的可能性,人们发明了最出色的东西,例如类型系统中功能齐全的 SQL 解析器或字典。我们使用条件类型作为一种工具,使静态类型系统在动态情况下更灵活。
Chapter 5, “Conditional Types” explains why the TypeScript type system is also its own metaprogramming language. With the possibility of choosing types based on certain conditions, people invented the most outstanding things, like a full-fledged SQL parser or a dictionary in the type system. We use conditional types as a tool to make a static type system more flexible for dynamic situations.
在第 6 章“字符串模板文字类型”中,您将了解 TypeScript 如何在类型系统中集成字符串解析器。从格式字符串中提取名称、根据字符串输入定义动态事件系统以及动态创建标识符:似乎没有什么是不可能的!
In Chapter 6, “String Template Literal Types” you see how TypeScript integrates a string parser in the type system. Extracting names from format strings, defining a dynamic event system based on string input, and creating identifiers dynamically: nothing seems impossible!
在第 7 章“可变元组类型”中,您可以稍微了解一下函数式编程。元组在 TypeScript 中具有特殊含义,有助于描述函数参数和类似对象的数组,并创建灵活的辅助函数。
You get a little taste of functional programming in Chapter 7, “Variadic Tuple Types”. The tuple has a special meaning in TypeScript and helps describe function parameters and object-like arrays, and it creates flexible helper functions.
第 8 章“辅助类型”中介绍了更多元编程。TypeScript 有一些内置辅助类型,可让您更轻松地从其他类型派生类型。在本章中,您不仅可以学习如何使用它们,还可以学习如何创建自己的辅助类型。本章还标志着TypeScript Cookbook中的下一个断点,因为此时您已经学习了语言和类型系统的所有基本要素,然后可以在下一部分中应用它们。
Even more metaprogramming happens in Chapter 8, “Helper Types”. TypeScript has a few built-in helper types that make it easier for you to derive types from other types. In this chapter, you learn not only how to use them but also how to create your own. This chapter also marks the next breakpoint in TypeScript Cookbook because at this point you have learned all the basic ingredients of the language and type system, which you then can apply in the next part.
在花了八章了解类型系统的所有细节之后,是时候将您的知识与第 9 章“标准库 和外部类型定义”中其他人完成的类型定义相结合了。在本章中,您将看到与预期不同的情况,并了解如何根据您的意愿调整内置类型定义。
After spending eight chapters understanding all the nitty-gritty of the type system, it’s time to integrate your knowledge with type definitions done by others in Chapter 9, “The Standard Library and External Type Definitions”. In this chapter you will see situations that work differently than expected, and see how you can bend the built-in type definitions to your will.
在第 10 章“TypeScript 和 React”中,你将了解最流行的 JavaScript 框架之一如何集成到 TypeScript 中,使语法扩展JSX成为可能的功能,以及它如何融入 TypeScript 的整体概念。你还将学习如何为组件和钩子编写健壮的类型,以及如何在事后处理已附加到实际库的类型定义文件。
In Chapter 10, “TypeScript and React” you will learn how one of the most popular JavaScript frameworks is integrated in TypeScript, features that make the syntax extension JSX possible, and how this fits into the overall concept of TypeScript. You will also learn how to write robust types for components and hooks, and how to deal with a type definition file that has been attached to the actual library after the fact.
下一章是关于类的,类是面向对象编程的一个基本要素,早在 JavaScript 中出现之前,TypeScript 中就已经存在了。这导致了第 11 章“类”中详细讨论的有趣功能的二元性。
The next chapter is about classes, a staple of object-oriented programming that was available in TypeScript long before their counterpart existed in JavaScript. This leads to an interesting duality of features discussed in detail in Chapter 11, “Classes”.
本书以第 12 章“类型开发策略”结束。在这里,我专注于让您掌握自己创建高级类型的技能,做出正确的决定来推进项目,并处理为您验证类型的库。您还将了解特殊的解决方法和隐藏功能,并讨论如何命名泛型或高级类型是否有点太多。这一章特别有趣,因为经过从新手到学徒的漫长旅程,您将达到专家 地位。
The book ends with Chapter 12, “Type Development Strategies”. Here I focus on giving you the skills to create advanced types on your own, to make the right decisions on how to move your project along, and to deal with libraries that validate types for you. You also will learn about special workarounds and hidden features, and discuss how to name generics or if advanced types are a bit too much. This chapter is particularly fun because after a long journey from novice to apprentice, you will reach expert status.
所有示例都可以在本书的网站上以 TypeScript 操场或 CodeSandbox 项目的形式获得。操场特别提供了一个中间状态,因此您可以自己摆弄并尝试这些行为。我总是说,您不能仅通过阅读来学习一门编程语言;您需要积极地编写代码并亲自动手才能了解一切是如何协同工作的。将此视为享受编程类型乐趣的邀请。
All examples are available as a TypeScript playground or CodeSandbox project at the book’s website. The playgrounds in particular offer an intermediate state, so you can fiddle around on your own and play with the behaviors. I always say that you can’t learn a programming language just by reading about it; you need to actively code and get your hands dirty to understand how everything plays together. See this as an invitation to have fun with programming types.
TypeScript 允许多种编程风格和格式选项。为了避免bike-shedding,我选择使用Prettier自动格式化所有示例。如果您习惯于不同的格式样式(也许您更喜欢在类型的每个属性声明后使用逗号而不是分号),那么您可以继续使用您的偏好。
TypeScript allows for many programming styles and formatting options. To avoid bike-shedding, I chose to autoformat all examples using Prettier. If you are used to a different formatting style—maybe you prefer commas instead of semicolons after each property declaration of your types—you are more than welcome to continue with your preference.
TypeScript Cookbook有很多示例,涉及很多函数。编写函数的方法有很多种,我选择主要编写函数声明而不是函数表达式,除非需要解释两种符号之间的差异。在所有其他情况下,这主要是出于个人喜好,而不是技术原因。
TypeScript Cookbook has a lot of examples and deals with a lot of functions. There are many ways to write functions, and I’ve chosen to write mostly function declarations instead of function expressions, except where it was crucial to explain the differences between both notations. On all other occasions, it’s mostly a matter of taste rather than for technical reasons.
所有示例均已针对本书撰写时的最新版本 TypeScript 5.0 进行了检查。TypeScript 不断变化,规则也是如此。本书确保我们主要关注持久且可跨版本信赖的事物。对于我预计会进一步发展或根本性变化的地方,我会提供相应的警告和说明。
All examples have been checked against TypeScript 5.0, the most recent release at the time of this book’s writing. TypeScript changes constantly and so do the rules. This book ensures that we mostly focus on things that are long-lasting and can be trusted across versions. Where I expect further development or fundamental change, I provide respective warnings and notes.
本书采用了以下印刷约定:
The following typographical conventions are used in this book:
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
Indicates new terms, URLs, email addresses, filenames, and file extensions.
Constant widthConstant width用于程序列表,以及段落内引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords.
Constant width italicConstant width italic显示应由用户提供的值或由上下文确定的值替换的文本。
Shows text that should be replaced with user-supplied values or by values determined by context.
这个元素表示提示或建议。
This element signifies a tip or suggestion.
此元素表示一般说明。
This element signifies a general note.
此元素表示警告或警示。
This element indicates a warning or caution.
补充材料(代码示例、练习等)可在https://typescript-cookbook.com下载。
Supplemental material (code examples, exercises, etc.) is available for download at https://typescript-cookbook.com.
如果您有技术问题或使用代码示例时遇到问题,请发送电子邮件至support@oreilly.com。
If you have a technical question or a problem using the code examples, please send email to support@oreilly.com.
本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在程序和文档中使用它。除非您要复制大量代码,否则无需联系我们获取许可。例如,编写使用本书中几段代码的程序不需要许可。出售或分发 O'Reilly 书籍中的示例则需要许可。通过引用本书并引用示例代码来回答问题不需要许可。将本书中的大量示例代码合并到您的产品文档中则需要 许可。
This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission.
我们欢迎但并不要求注明出处。注明出处通常包括书名、作者、出版商和 ISBN。例如:“ Stefan Baumgartner (O'Reilly) 著的TypeScript Cookbook 。版权所有 2023 Stefan Baumgartner,978-1-098-13665-9。”
We appreciate, but do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “TypeScript Cookbook by Stefan Baumgartner (O’Reilly). Copyright 2023 Stefan Baumgartner, 978-1-098-13665-9.”
如果您认为您对代码示例的使用超出了合理使用或上述许可的范围,请随时通过permissions@oreilly.com与我们联系。
If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com.
40 多年来,O'Reilly Media一直提供技术和商业培训、知识和见解,帮助企业取得成功。
For more than 40 years, O’Reilly Media has provided technology and business training, knowledge, and insight to help companies succeed.
我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专长。O'Reilly 的在线学习平台让您可以按需访问现场培训课程、深度学习路径、交互式编码环境以及来自 O'Reilly 和 200 多家其他出版商的大量文本和视频。有关更多信息,请访问https://oreilly.com。
Our unique network of experts and innovators share their knowledge and expertise through books, articles, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O’Reilly and 200+ other publishers. For more information, visit https://oreilly.com.
请将有关本书的评论和问题发送给出版商:
Please address comments and questions concerning this book to the publisher:
我们为这本书建立了一个网页,其中列出了勘误表、示例和任何其他信息。您可以通过https://oreil.ly/typescript-cookbook访问此页面。
We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at https://oreil.ly/typescript-cookbook.
有关我们的书籍和课程的新闻和信息,请访问https://oreilly.com。
For news and information about our books and courses, visit https://oreilly.com.
在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media。
Find us on LinkedIn: https://linkedin.com/company/oreilly-media.
在 Twitter 上关注我们:https://twitter.com/oreillymedia。
Follow us on Twitter: https://twitter.com/oreillymedia.
在 YouTube 上观看我们:https://youtube.com/oreillymedia。
Watch us on YouTube: https://youtube.com/oreillymedia.
每当我有新菜品要烹饪时,我都会首先咨询 Alexander Rosemann、Sebastian Gierlinger、Dominik Angerer 和 Georg Kothmeier。我们定期的会面和互动不仅很有趣,而且还为我提供了评估所有选择的必要反馈。他们是第一批听说这本书的人,也是第一批给出反馈的人。
Alexander Rosemann, Sebastian Gierlinger, Dominik Angerer, and Georg Kothmeier are the first people I go to if I have something new cooking. Our regular meetings and interactions not only are entertaining but also provide me with the necessary feedback to evaluate all my choices. They are the first people that heard about the book, and also the first ones that gave feedback.
在社交媒体上与 Matt Pocock、Joe Previte、Dan Vanderkam、Nathan Shively-Sanders 和 Josh Goldberg 的互动带来了许多新想法。他们对 TypeScript 的看法可能与我不同,但最终拓宽了我的视野,并确保我不会过于固执己见。
Interacting with Matt Pocock, Joe Previte, Dan Vanderkam, Nathan Shively-Sanders, and Josh Goldberg on social media brought plenty of new ideas to the table. Their approach to TypeScript might differ from mine, but they ultimately broadened my horizon and made sure that I didn’t end up too opinionated.
Phil Nash、Simona Cotin 和 Vanessa Böhner 不仅是最终手稿的早期审阅者,也是我多年的伙伴和朋友,他们总是在这里检查我的想法是否合理。Addy Osmani 在我的整个职业生涯中一直都是我的灵感源泉,我很自豪他同意为我的新书开稿。
Phil Nash, Simona Cotin, and Vanessa Böhner have not only been early reviewers of the final manuscript but also long-time companions and friends who are always here to sanity-check my ideas. Addy Osmani has been an inspiration throughout my entire career, and I’m very proud that he agreed to open my new book.
Lena Matscheko、Alexandra Rapeanu 和 Mike Kuss 毫不犹豫地向我提出了许多基于他们实际经验的技术挑战和问题。当我缺乏一个很好的例子时,他们就用大量优秀的素材来提炼。
Lena Matscheko, Alexandra Rapeanu, and Mike Kuss did not hesitate to bombard me with technical challenges and questions based on their real-world experiences. Where I lacked a good example, they flooded me with excellent source material to distill.
如果不是 Peter Kröner,我可能就跟不上 TypeScript 的所有发展了。每当有新的 TypeScript 版本发布时,他总会敲我的门。我们一起制作的有关 TypeScript 发布的播客节目非常有名,而且越来越少涉及 TypeScript。
I would lose track of all of TypeScript’s developments if it wasn’t for Peter Kröner, who constantly knocks on my door when there’s a new TypeScript version coming out. Our podcast episodes together on TypeScript releases are legendary, and also increasingly not about TypeScript.
我的技术编辑 Mark Halpin、Fabian Friedl 和 Bernhard Mayr 提供了我所期望的最佳技术反馈。他们质疑每一个假设,检查每一个代码示例,并确保我的所有推理都合理,没有遗漏任何一个细节。他们对细节的热爱和高水平讨论的能力确保了这本书不仅仅是又一本热门话题的合集,而是一个有坚实基础的指南和参考书。
My tech editors Mark Halpin, Fabian Friedl, and Bernhard Mayr provided the best technical feedback I could wish for. They challenged every assumption, checked on every code sample, and made sure all my reasoning made sense and that I didn’t skip a beat. Their love of detail and their ability to discuss on such a high level ensured that this book is not just another collection of hot takes but a guide and reference that stands on a solid foundation.
如果没有 Amanda Quinn,这本书就不会存在。在 2020 年写完《TypeScript 50 课》后,我以为我已经说完了关于 TypeScript 的所有内容。是 Amanda 鼓励我尝试编写一本烹饪书,看看哪些想法不适合我的第一本书。三个小时后,我写出了一份完整的提案和目录,其中包含一百多个条目。Amanda 是对的:我还有很多话要说,我永远感谢她的 支持和指导。
This book would not exist if not for Amanda Quinn. After writing TypeScript in 50 Lessons in 2020, I thought I’d said everything I needed to say about TypeScript. It was Amanda who pursued me to give the idea of a cookbook a go, to see which ideas I would find that wouldn’t make the cut for my first book. After three hours I had a complete proposal and table of contents with more than one hundred entries. Amanda was right: I had so much more to say, and I’m eternally grateful for her support and her guidance.
Amanda 在早期阶段提供了帮助,而 Shira Evans 则确保项目进展顺利,不会脱轨。她的反馈非常宝贵,她务实、亲力亲为的态度让大家一起工作非常愉快。
Where Amanda helped in the early phases, Shira Evans made sure that the project made good progress and didn’t derail. Her feedback was invaluable, and her pragmatic and hands-on approach made it a joy to work together.
Elizabeth Faerm 和 Theresa Jones 负责制作。他们对细节的关注非常出色,他们确保制作阶段令人兴奋,而且实际上非常有趣!最终的结果是一次让我百看不厌的美妙体验。
Elizabeth Faerm and Theresa Jones took care of the production. Their eye for detail is outstanding, and they made sure that the production phase is exciting and actually a lot of fun! The final result is a beautiful experience I can’t get enough of.
在写作过程中,我得到了 Porcupine Tree、Beck、Nobuo Uematsu、Camel、The Beta Band 和其他许多人的大力帮助。
During writing I had great assistance from Porcupine Tree, Beck, Nobuo Uematsu, Camel, The Beta Band, and many others.
这本书的最大贡献来自我的家人。多丽丝、克莱门斯和亚伦是我一直以来的愿望,没有他们无尽的爱和支持,我就无法实现我的抱负。感谢你们所做的一切。
The biggest contribution to this book comes from my family. Doris, Clemens, and Aaron are everything I’ve ever wished for, and without their endless love and support, I wouldn’t be able to pursue my ambitions. Thank you for everything.
您想开始使用 TypeScript,太棒了!最大的问题是:如何开始?您可以通过多种方式将 TypeScript 集成到您的项目中,并且根据项目的需求,所有方式都略有不同。就像 JavaScript 在许多运行时上运行一样,有很多方法可以配置 TypeScript 以满足您的目标需求。
You want to get started with TypeScript, fantastic! The big question is: how do you start? You can integrate TypeScript into your projects in many ways, and all are slightly different depending on your project’s needs. Just as JavaScript runs on many runtimes, there are plenty of ways to configure TypeScript so it meets your target’s needs.
本章介绍了将 TypeScript 引入项目的所有可能性,作为 JavaScript 的扩展,为您提供基本的自动完成和错误指示,直至 Node.js 和浏览器上全栈应用程序的完整设置。
This chapter covers all the possibilities of introducing TypeScript to your project, as an extension next to JavaScript that gives you basic autocompletion and error indication, up to full-fledged setups for full-stack applications on Node.js and the browser.
由于 JavaScript 工具是一个具有无限可能性的领域——有人说每周都会发布一个新的 JavaScript 构建链,几乎与新框架一样多——本章更多地关注仅使用 TypeScript 编译器就可以做什么,而无需任何额外的工具。
Since JavaScript tooling is a field with endless possibilities—some say that a new JavaScript build chain is released every week, almost as much as new frameworks—this chapter focuses more on what you can do with the TypeScript compiler alone, without any extra tool.
TypeScript满足了您的所有转译需求,除了创建最小化和优化的打包文件以供网络分发之外。ESBuild或Webpack等打包工具可以完成这项任务。此外,还有一些设置包括其他转译工具,例如可以与 TypeScript 完美配合的Babel.js。
TypeScript offers everything you need for your transpilation needs, except the ability to create minified and optimized bundles for web distribution. Bundlers like ESBuild or Webpack take care of this task. Also, there are setups that include other transpilers like Babel.js that can play nicely with TypeScript.
打包器和其他转译器不在本章的讨论范围内。请参阅其文档以了解 TypeScript 的包含情况,并使用本章中的知识来获取正确的配置设置。
Bundlers and other transpilers are not within the scope of this chapter. Refer to their documentation for the inclusion of TypeScript and use the knowledge in this chapter to get the right configuration setup.
TypeScript 是一个拥有十多年历史的项目,它保留了一些旧时代的遗留内容,为了兼容性,TypeScript 不能简单地将其删除。因此,本章将重点介绍现代 JavaScript 语法和 Web 标准的最新发展。
TypeScript being a project with more than a decade of history, it carries some remains from older times that, for the sake of compatibility, TypeScript can’t just get rid of. Therefore, this chapter will spotlight modern JavaScript syntax and recent developments in web standards.
如果您仍需要针对 Internet Explorer 8 或 Node.js 10,首先:很抱歉,这些平台确实很难开发。但是,其次:您将能够利用本章和官方 TypeScript 文档中的知识为较旧的平台整合各个部分。
If you still need to target Internet Explorer 8 or Node.js 10, first: I’m sorry, these platforms are really hard to develop for. However, second: you will be able to put together the pieces for older platforms with the knowledge from this chapter and the official TypeScript documentation.
在要进行类型检查的每个 JavaScript 文件的开头添加一行注释@ts-check。使用正确的编辑器,当 TypeScript 遇到不合逻辑的内容时,您就会看到红色波浪线。
Add a single-line comment with @ts-check at the beginning of every JavaScript file you want to type-check. With the right editors, you already get red squiggly lines whenever TypeScript encounters things that don’t quite add up.
TypeScript 被设计为 JavaScript 的超集,并且每个有效的 JavaScript 也是有效的 TypeScript。这意味着 TypeScript 也非常擅长找出常规 JavaScript 代码中的潜在错误。
TypeScript has been designed as a superset of JavaScript, and every valid JavaScript is also valid TypeScript. This means TypeScript is also really good at figuring out potential errors in regular JavaScript code.
如果我们不想使用完整的 TypeScript 设置,但想要一些基本的提示和类型检查来简化我们的开发工作流程,我们可以使用它。
We can use this if we don’t want a full-blown TypeScript setup but want some basic hints and type-checks to ease our development workflow.
如果您只想对 JavaScript 进行类型检查,那么一个很好的先决条件就是一个好的编辑器或 IDE。与 TypeScript 非常匹配的编辑器是Visual Studio Code。Visual Studio Code(简称 VSCode)是第一个使用 TypeScript 的主要项目,甚至在 TypeScript 发布之前就已存在。
A good prerequisite if you only want to type-check JavaScript is a good editor or IDE. An editor that goes really well with TypeScript is Visual Studio Code. Visual Studio Code—or VSCode for short—was the first major project to utilize TypeScript, even before TypeScript’s release.
如果您想编写 JavaScript 或 TypeScript,很多人会推荐 VSCode。但实际上,只要支持 TypeScript,每个编辑器都很棒。如今大多数编辑器都支持 TypeScript。
A lot of people recommend VSCode if you want to write JavaScript or TypeScript. But really, every editor is great as long as it features TypeScript support. And nowadays most of them do.
使用 Visual Studio Code,我们可以获得一个非常重要的 JavaScript 类型检查功能:当某些内容不太对劲时,会显示红色波浪线,如图1-1所示。这是最低的入门门槛。TypeScript 的类型系统在使用代码库时具有不同级别的严格性。
With Visual Studio Code we get one very important thing for type-checking JavaScript: red squiggly lines when something doesn’t quite add up, as you can see in Figure 1-1. This is the lowest barrier to entry. TypeScript’s type system has different levels of strictness when working with a codebase.
首先,类型系统将尝试通过使用 JavaScript 代码推断类型。如果你的代码中有这样一行:
First, the type system will try to infer types from JavaScript code through usage. If you have a line like this in your code:
leta_number=1000;
leta_number=1000;
TypeScript 将正确推断number为 的类型a_number。
TypeScript will correctly infer number as the type of a_number.
JavaScript 的一个难点是类型是动态的。通过 、 或 的绑定let可以var根据const使用情况更改类型。1看一下以下示例:
One difficulty with JavaScript is that types are dynamic. Bindings via let, var, or const can change type based on usage.1 Take a look at the following example:
leta_number=1000;if(Math.random()<0.5){a_number="Hello, World!";}console.log(a_number*10);
leta_number=1000;if(Math.random()<0.5){a_number="Hello, World!";}console.log(a_number*10);
如果下一行中的条件计算结果为真,我们将一个数字分配给a_number并将绑定更改为string。如果我们没有尝试在最后一行进行乘法运算,这不会有什么大问题a_number。在所有情况下,大约 50% 的情况下,此示例会产生不良行为。
We assign a number to a_number and change the binding to a string if the condition in the next line evaluates to true. This wouldn’t be much of a problem if we didn’t try to multiply a_number on the last line. In approximately 50% of all cases, this example will produce unwanted behavior.
TypeScript 可以在这里提供帮助。@ts-check通过在 JavaScript 文件的最顶部
添加单行注释,TypeScript 激活了下一个严格级别:根据JavaScript文件中可用的类型信息对 JavaScript 文件进行类型检查。
TypeScript can help here. With the addition of a single-line comment with @ts-check at the very top of our JavaScript file, TypeScript activates the next strictness level: type-checking JavaScript files based on the type information available in the
JavaScript file.
在我们的示例中,TypeScript 会发现我们试图将一个字符串分配给 TypeScript 推断为数字的绑定。我们将在编辑器中收到错误:
In our example, TypeScript will figure out that we tried to assign a string to a binding that TypeScript has inferred to be a number. We will get an error in our editor:
// @ts-checkleta_number=1000;if(Math.random()<0.5){a_number="Hello, World!";// ^-- Type 'string' is not assignable to type 'number'.ts(2322)}console.log(a_number*10);
// @ts-checkleta_number=1000;if(Math.random()<0.5){a_number="Hello, World!";// ^-- Type 'string' is not assignable to type 'number'.ts(2322)}console.log(a_number*10);
现在我们可以开始修复我们的代码了,TypeScript 将指导我们。
Now we can start to fix our code, and TypeScript will guide us.
JavaScript 的类型推断大有裨益。在以下示例中,TypeScript 通过查看乘法和加法等运算以及默认值来推断类型:
Type inference for JavaScript goes a long way. In the following example, TypeScript infers types by looking at operations like multiplication and addition as well as default values:
functionaddVAT(price,vat=0.2){returnprice*(1+vat);}
functionaddVAT(price,vat=0.2){returnprice*(1+vat);}
该函数addVat接受两个参数。第二个参数是可选的,因为它已被设置为默认值0.2。如果您尝试传递无效的值,TypeScript 会提醒您:
The function addVat takes two arguments. The second argument is optional, as it has been set to a default value of 0.2. TypeScript will alert you if you try to pass a value that doesn’t work:
addVAT(1000,"a string");// ^-- Argument of type 'string' is not assignable// to parameter of type 'number'.ts(2345)
addVAT(1000,"a string");// ^-- Argument of type 'string' is not assignable// to parameter of type 'number'.ts(2345)
此外,由于我们在函数体中使用乘法和加法运算,TypeScript 知道我们将从该函数返回一个数字:
Also, since we use multiplication and addition operations within the function body, TypeScript understands that we will return a number from this function:
addVAT(1000).toUpperCase();// ^-- Property 'toUpperCase' does not// exist on type 'number'.ts(2339)
addVAT(1000).toUpperCase();// ^-- Property 'toUpperCase' does not// exist on type 'number'.ts(2339)
在某些情况下,您需要的不仅仅是类型推断。在 JavaScript 文件中,您可以通过JSDoc类型注释来注释函数参数和绑定。JSDoc是一种注释约定,它允许您以一种不仅人类可读而且机器可解释的方式描述变量和函数 接口。TypeScript 将拾取您的注释并将其用作类型系统的类型:
In some situations you need more than type inference. In JavaScript files, you can annotate function arguments and bindings through JSDoc type annotations. JSDoc is a comment convention that allows you to describe your variables and function interfaces in a way that’s not only readable for humans but also interpretable by machines. TypeScript will pick up your annotations and use them as types for the type system:
/** @type {number} */letamount;amount='12';// ^-- Argument of type 'string' is not assignable// to parameter of type 'number'.ts(2345)/*** Adds VAT to a price** @param {number} price The price without VAT* @param {number} vat The VAT [0-1]** @returns {number}*/functionaddVAT(price,vat=0.2){returnprice*(1+vat);}
/** @type {number} */letamount;amount='12';// ^-- Argument of type 'string' is not assignable// to parameter of type 'number'.ts(2345)/*** Adds VAT to a price** @param {number} price The price without VAT* @param {number} vat The VAT [0-1]** @returns {number}*/functionaddVAT(price,vat=0.2){returnprice*(1+vat);}
JSDoc 还允许您为对象定义新的复杂类型:
JSDoc also allows you to define new, complex types for objects:
/*** @typedef {Object} Article* @property {number} price* @property {number} vat* @property {string} string* @property {boolean=} sold*//*** Now we can use Article as a proper type* @param {[Article]} articles*/functiontotalAmount(articles){returnarticles.reduce((total,article)=>{returntotal+addVAT(article);},0);}
/*** @typedef {Object} Article* @property {number} price* @property {number} vat* @property {string} string* @property {boolean=} sold*//*** Now we can use Article as a proper type* @param {[Article]} articles*/functiontotalAmount(articles){returnarticles.reduce((total,article)=>{returntotal+addVAT(article);},0);}
虽然语法可能感觉有点笨拙;我们将在方案 1.3中找到更好的方法来注释对象。
The syntax might feel a bit clunky, though; we will find better ways to annotate objects in Recipe 1.3.
假设您有一个通过 JSDoc 详细记录的 JavaScript 代码库,那么在文件顶部添加一行将使您能够很好地了解代码中是否出现问题。
Given that you have a JavaScript codebase that is well documented via JSDoc, adding a single line on top of your files will give you a really good understanding if something goes wrong in your code.
TypeScript 用 TypeScript 编写,编译为 JavaScript,并使用Node.js JavaScript 运行时作为其主要执行环境。2即使您没有编写 Node.js 应用程序,JavaScript 应用程序的工具也将在 Node 上运行。因此,请确保从官方网站获取 Node.js并熟悉其命令行工具。
TypeScript is written in TypeScript, compiled to JavaScript, and uses the Node.js JavaScript runtime as its primary execution environment.2 Even if you’re not writing a Node.js app, the tooling for your JavaScript applications will run on Node. So, make sure you get Node.js from the official website and get familiar with its command-line tools.
对于新项目,请确保使用新的package.json初始化项目文件夹 。此文件包含 Node 及其包管理器 NPM 的所有信息,以确定项目的内容。使用 NPM 命令行工具在项目文件夹中生成一个具有默认内容的新package.json文件:
For a new project, make sure you initialize your project’s folder with a fresh package.json. This file contains all the information for Node and its package manager NPM to figure out your project’s contents. Generate a new package.json file with default contents in your project’s folder with the NPM command-line tool:
$npminit-y
$npminit-y
在本书中,您将看到应在终端中执行的命令。为方便起见,我们按 BASH 或适用于 Linux、macOS 或适用于 Linux 的 Windows 子系统的类似 shell 中显示的方式显示这些命令。前导$符号是指示命令的惯例,但并不意味着由您编写。请注意,所有命令也适用于常规
Windows命令行界面以及 PowerShell。
Throughout this book, you will see commands that should be executed in your terminal. For convenience, we show these commands as they would appear on BASH or similar shells available for Linux, macOS, or the Windows subsystem for Linux. The leading $ sign is a convention to indicate a command, but it is not meant to be written by you. Note that all commands also work on the regular
Windows command-line interface as well as PowerShell.
NPM 是 Node 的包管理器。它带有 CLI、注册表和其他允许您安装依赖项的工具。初始化package.json后,从 NPM 安装 TypeScript 。我们将其安装为开发依赖项,这意味着如果您打算将项目作为库发布到NPM 本身,则不会包含 TypeScript :
NPM is Node’s package manager. It comes with a CLI, a registry, and other tools that allow you to install dependencies. Once you initialize your package.json, install TypeScript from NPM. We install it as a development dependency, meaning that TypeScript won’t be included if you intend to publish your project as a library to NPM itself:
$npminstall-Dtypescript
$npminstall-Dtypescript
您可以全局安装 TypeScript,这样 TypeScript 编译器就可以在任何地方使用,但我强烈建议每个项目单独安装 TypeScript。根据您访问项目的频率,您最终会得到与项目代码同步的不同 TypeScript 版本。全局安装(和更新)TypeScript 可能会破坏您有一段时间没碰过的项目。
You can globally install TypeScript so you have the TypeScript compiler available everywhere, but I strongly suggest installing TypeScript separately per project. Depending on how frequently you visit your projects, you will end up with different TypeScript versions that are in sync with your project’s code. Installing (and updating) TypeScript globally might break projects you haven’t touched in a while.
如果你通过 NPM 安装前端依赖项,则需要一个额外的工具来确保你的代码也可以在浏览器中运行:打包器。TypeScript 不包含与支持的模块系统配合使用的打包器,因此你需要设置适当的工具。Webpack 等工具很常见, ESBuild也是如此。所有工具都设计为也可以执行 TypeScript。或者你可以完全使用原生版本,如方案1.8中所述。
If you install frontend dependencies via NPM, you will need an additional tool to make sure that your code also runs in your browser: a bundler. TypeScript doesn’t include a bundler that works with the supported module systems, so you need to set up the proper tooling. Tools like Webpack are common, and so is ESBuild. All tools are designed to execute TypeScript as well. Or you can go full native, as described in Recipe 1.8.
现在 TypeScript 已安装完毕,请初始化一个新的 TypeScript 项目。为此,请使用 NPX:它允许您执行相对于项目安装的命令行实用程序。
Now that TypeScript is installed, initialize a new TypeScript project. Use NPX for that: it allows you to execute a command-line utility that you installed relative to your project.
和:
With:
$npxtsc——初始化
$npxtsc--init
您可以运行项目的本地版本的 TypeScript 编译器并传递init标志来创建一个新的tsconfig.json。
you can run your project’s local version of the TypeScript compiler and pass the init flag to create a new tsconfig.json.
tsconfig.json是 TypeScript 项目的主要配置文件。它包含所需的所有配置,以便 TypeScript 了解如何解释代码、如何使类型可用于依赖项,以及是否需要打开或关闭某些功能。
The tsconfig.json is the main configuration file for your TypeScript project. It contains all the configuration needed so that TypeScript understands how to interpret your code, how to make types available for dependencies, and if you need to turn certain features on or off.
默认情况下,TypeScript 为您设置以下选项:
Per default, TypeScript sets these options for you:
{"compilerOptions":{"target":"es2016","module":"commonjs","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"skipLibCheck":true}}
{"compilerOptions":{"target":"es2016","module":"commonjs","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"skipLibCheck":true}}
让我们详细地看一下。
Let’s look at them in detail.
target是es2016,这意味着如果你运行 TypeScript 编译器,它会将你的 TypeScript 文件编译为与 ECMAScript 2016 兼容的语法。根据你支持的浏览器或环境,你可以将其设置为较新的版本(ECMAScript 版本以发布年份命名)或较旧的版本,例如es5对于必须支持非常旧的 Internet Explorer 版本的人。当然,我希望你不必这样做。
target is es2016, which means that if you run the TypeScript compiler, it will compile your TypeScript files to an ECMAScript 2016 compatible syntax. Depending on your supported browsers or environments, you can set that either to something more recent (ECMAScript versions are named after the year of release) or to something older such as es5 for people who have to support very old Internet Explorer versions. Of course, I hope you don’t have to.
module是commonjs。这允许您编写 ECMAScript 模块语法,但 TypeScript 不会将此语法带到输出,而是将其编译为 CommonJS 格式。这意味着:
module is commonjs. This allows you to write ECMAScript module syntax, but instead of carrying this syntax over to the output, TypeScript will compile it to the CommonJS format. This means that:
import{name}from"./my-module";console.log(name);//...
import{name}from"./my-module";console.log(name);//...
变成:
becomes:
constmy_module_1=require("./my-module");console.log(my_module_1.name);
constmy_module_1=require("./my-module");console.log(my_module_1.name);
一旦编译完成。CommonJS 是 Node.js 的模块系统,由于 Node 的流行,它变得非常普遍。Node.js 也采用了 ECMAScript 模块,我们将在1.9 节中讨论这个问题。
once you compile. CommonJS was the module system for Node.js and has become very common because of Node’s popularity. Node.js has since adopted ECMAScript modules as well, something we’ll tackle in Recipe 1.9.
esModuleInterop确保非 ECMAScript 模块在导入后与标准保持一致。forceConsistentCasingInFileNames帮助使用区分大小写的文件系统的人与使用不区分大小写的文件系统的人合作。并skipLibCheck假设您安装的类型定义文件(稍后会详细介绍)没有错误。因此您的编译器不会检查它们,并且会变得更快一些。
esModuleInterop ensures modules that aren’t ECMAScript modules are aligned to the standard once imported. forceConsistentCasingInFileNames helps people using case-sensitive file systems cooperate with folks who use case-insensitive file systems. And skipLibCheck assumes that your installed type definition files (more on that later) have no errors. So your compiler won’t check them and will become a little faster.
最有趣的功能之一是 TypeScript 的严格模式。如果设置为true,TypeScript 将在某些领域表现不同。这是 TypeScript 团队定义类型系统应如何表现的一种方式。
One of the most interesting features is TypeScript’s strict mode. If set to true, TypeScript will behave differently in certain areas. It’s a way for the TypeScript team to define their view on how the type system should behave.
如果 TypeScript 因类型系统视图发生变化而引入重大更改,它将被纳入严格模式。这最终意味着,如果您更新 TypeScript 并始终在严格模式下运行,您的代码可能会崩溃。
If TypeScript introduces a breaking change because the view on the type system changes, it will get incorporated in strict mode. This ultimately means that your code might break if you update TypeScript and always run in strict mode.
为了让你有时间适应变化,TypeScript 还允许您逐个打开或关闭某些严格模式功能。
To give you time to adapt to changes, TypeScript also allows you to turn certain strict mode features on or off, feature by feature.
除了默认设置外,我强烈建议另外设置两个:
In addition to the default settings, I strongly recommend two more:
{"compilerOptions":{//..."rootDir":"./src","outDir":"./dist"}}
{"compilerOptions":{//..."rootDir":"./src","outDir":"./dist"}}
这告诉 TypeScript 从src文件夹中获取源文件,并将编译后的文件放入dist文件夹中。此设置允许您将构建的文件与您编写的文件分开。当然,您必须创建src文件夹; dist文件夹将在编译后创建。
This tells TypeScript to pick up source files from a src folder and put the compiled files into a dist folder. This setup allows you to separate your built files from the ones you author. You will have to create the src folder, of course; the dist folder will be created after you compile.
哦,编译。设置好项目后,在以下位置创建一个index.ts文件src:
Oh, compilation. Once you have your project set up, create an index.ts file in src:
console.log("Hello World");
console.log("Hello World");
.ts扩展名表示这是一个 TypeScript 文件。现在运行:
The .ts extension indicates it’s a TypeScript file. Now run:
$npxtsc
$npxtsc
您希望编写常规 JavaScript,无需额外的构建步骤,但仍能获得一些编辑器支持和函数的正确类型信息。但是,您不想使用 JSDoc 定义复杂的对象类型,如方案 1.1所示。
You want to write regular JavaScript with no extra build step but still get some editor support and proper type information for your functions. However, you don’t want to define your complex object types with JSDoc as shown in Recipe 1.1.
将类型定义文件“放在一边”,并以“检查 JavaScript”模式运行 TypeScript 编译器。
Keep type definition files “on the side” and run the TypeScript compiler in the “check JavaScript” mode.
逐步采用一直是 TypeScript 的专门目标。借助这种我称之为“侧面类型”的技术,您可以为对象类型和泛型和条件类型等高级功能编写 TypeScript 语法(参见第 5 章),而不必使用笨重的 JSDoc 注释,但您仍可以为实际应用编写 JavaScript。
Gradual adoption has always been a dedicated goal for TypeScript. With this technique, which I dubbed “types on the side,” you can write TypeScript syntax for object types and advanced features like generics and conditional types (see Chapter 5) instead of clunky JSDoc comments, but you still write JavaScript for your actual app.
在项目的某个地方(可能是@types文件夹),创建一个类型定义文件。它的后缀是.d.ts,与常规.ts文件不同,它的目的是保存声明,而不是实际代码。
Somewhere in your project, maybe in a @types folder, create a type definition file. Its ending is .d.ts, and as opposed to regular .ts files, its purpose is to hold declarations but no actual code.
您可以在此处编写接口、类型别名和复杂类型:
This is where you can write your interfaces, type aliases, and complex types:
// @types/person.d.ts// An interface for objects of this shapeexportinterfacePerson{name:string;age:number;}// An interface that extends the original one// this is tough to write with JSDoc comments alone.exportinterfaceStudentextendsPerson{semester:number;}
// @types/person.d.ts// An interface for objects of this shapeexportinterfacePerson{name:string;age:number;}// An interface that extends the original one// this is tough to write with JSDoc comments alone.exportinterfaceStudentextendsPerson{semester:number;}
请注意,您从声明文件中导出接口。这样您就可以将它们导入到 JavaScript 文件中:
Note that you export the interfaces from the declaration files. This is so you can import them in your JavaScript files:
// index.js/** @typedef { import ("../@types/person").Person } Person */
// index.js/** @typedef { import ("../@types/person").Person } Person */
第一行的注释告诉 TypeScriptPerson从@types/person导入类型并以名称提供Person。
The comment on the first line tells TypeScript to import the Person type from @types/person and make it available under the name Person.
现在,您可以使用该标识符来注释函数参数或对象,就像使用原始类型一样string:
Now you can use this identifier to annotate function parameters or objects just like you would with primitive types like string:
// index.js, continued/*** @param {Person} person*/functionprintPerson(person){console.log(person.name);}
// index.js, continued/*** @param {Person} person*/functionprintPerson(person){console.log(person.name);}
为了确保获得编辑器反馈,你仍然需要@ts-check在 JavaScript 文件的开头进行设置,如方案 1.1中所述。或者,你可以将项目配置为始终检查 JavaScript。
To make sure that you get editor feedback, you still need to set @ts-check at the beginning of your JavaScript files as described in Recipe 1.1. Or, you can configure your project to always check JavaScript.
打开tsconfig.json并将checkJs标志设置为。这将从srctrue文件夹中获取所有 JavaScript 文件,并在编辑器中不断为您提供有关类型错误的反馈。您还可以运行以查看命令行中是否有错误。npx tsc
Open tsconfig.json and set the checkJs flag to true. This will pick up all the JavaScript files from your src folder and give you constant feedback on type errors in your editor. You also can run npx tsc to see if you have errors in your command line.
如果你不希望 TypeScript 将你的 JavaScript 文件转换为旧版本的 JavaScript,请确保设置noEmit为true:
If you don’t want TypeScript to transpile your JavaScript files to older versions of JavaScript, make sure you set noEmit to true:
{"compilerOptions":{"checkJs":true,"noEmit":true,}}
{"compilerOptions":{"checkJs":true,"noEmit":true,}}
这样,TypeScript 将查看你的源文件并为你提供所需的所有类型信息,但它不会触及你的代码。
With that, TypeScript will look at your source files and will give you all the type information you need, but it won’t touch your code.
这种技术也以可扩展性而闻名。Preact 等著名 JavaScript 库就是这样工作的,并为其用户和贡献者提供了出色的工具。
This technique is also known to scale. Prominent JavaScript libraries like Preact work like this and provide fantastic tooling for their users as well as their contributors.
将模块文件从.js重命名为.ts。使用多个编译器选项和功能来帮助您消除错误。
Rename your modules file by file from .js to .ts. Use several compiler options and features that help you iron out errors.
使用 TypeScript 文件而不是具有类型的 JavaScript 文件的好处是,您的类型和实现都在一个文件中,这为您提供了更好的编辑器支持和对更多 TypeScript 功能的访问,并提高了与其他工具的兼容性。
The benefit of having TypeScript files instead of JavaScript files with types is that your types and implementations are in one file, which gives you better editor support and access to more TypeScript features, and increases compatibility with other tools.
但是,将所有文件从.js重命名为.ts很可能会导致大量错误。这就是为什么你应该逐个文件进行操作并逐步提高类型安全性。
However, just renaming all files from .js to .ts most likely will result in tons of errors. This is why you should go file by file and gradually increase type safety as you go along.
迁移时最大的问题是你突然要处理 TypeScript 项目,而不是 JavaScript。不过,你的许多模块都是 JavaScript,如果没有类型信息,它们将无法通过类型检查步骤。
The biggest problem when migrating is that you’re suddenly dealing with a TypeScript project, not with JavaScript. Still, lots of your modules will be JavaScript and, with no type information, they will fail the type-checking step.
通过关闭 JavaScript 的类型检查,让您自己和 TypeScript 更容易,但允许 TypeScript 模块加载和引用 JavaScript 文件:
Make it easier for yourself and for TypeScript by turning off type-checking for JavaScript, but allow TypeScript modules to load and refer to JavaScript files:
{"compilerOptions":{"checkJs":false,"allowJs":true}}
{"compilerOptions":{"checkJs":false,"allowJs":true}}
现在运行npx tsc,你会看到 TypeScript 会获取源文件夹中的所有 JavaScript 和 TypeScript 文件,并在目标文件夹中创建相应的 JavaScript 文件。TypeScript 还会转译你的代码,使其与指定的目标版本兼容。
Should you run npx tsc now, you will see that TypeScript picks up all JavaScript and TypeScript files in your source folder and creates respective JavaScript files in your destination folder. TypeScript will also transpile your code to be compatible with the specified target version.
如果你正在使用依赖项,你会发现其中一些依赖项没有类型信息。这也会产生 TypeScript 错误:
If you are working with dependencies, you will see that some of them don’t come with type information. This will also produce TypeScript errors:
import_from"lodash";// ^- Could not find a declaration// file for module 'lodash'.
import_from"lodash";// ^- Could not find a declaration// file for module 'lodash'.
安装第三方类型定义可以解决此错误。请参阅方案 1.5。
Install third-party type definitions to get rid of this error. See Recipe 1.5.
一旦你逐个文件地迁移,你可能会意识到你无法一次性获得一个文件的所有类型。存在依赖关系,你很快就会陷入困境:有太多文件需要调整,然后你才能处理你真正需要的文件。
Once you migrate file by file, you might realize that you won’t be able to get all typings for one file in one go. There are dependencies, and you will quickly go down the rabbit hole of having too many files to adjust before you can tackle the one that you actually need.
你始终可以选择忍受错误。默认情况下,TypeScript 将编译器选项设置noEmitOnError为false:
You can always decide just to live with the error. By default, TypeScript sets the compiler option noEmitOnError to false:
{"compilerOptions":{"noEmitOnError":false}}
{"compilerOptions":{"noEmitOnError":false}}
这意味着无论你的项目中有多少错误,TypeScript 都会生成结果文件,尽量不阻碍你。这可能是你在完成迁移后想要打开的设置。
This means that no matter how many errors you have in your project, TypeScript will generate result files, trying not to block you. This might be a setting you want to turn on after you finish migrating.
在严格模式下,TypeScript 的功能标志noImplicitAny设置为true。此标志将确保您不会忘记为变量、常量或函数参数分配类型。即使只是any:
In strict mode, TypeScript’s feature flag noImplicitAny is set to true. This flag will make sure that you don’t forget to assign a type to a variable, constant, or function parameter. Even if it’s just any:
functionprintPerson(person:any){// This doesn't make sense, but is ok with anyconsole.log(person.gobbleydegook);}// This also doesn't make sense, but any allows itprintPerson(123);
functionprintPerson(person:any){// This doesn't make sense, but is ok with anyconsole.log(person.gobbleydegook);}// This also doesn't make sense, but any allows itprintPerson(123);
any是 TypeScript 中的万能类型。每个值都与 兼容any,并any允许您访问每个属性或调用每个方法。any有效地关闭类型检查,让您在迁移过程中有喘息的空间。
any is the catchall type in TypeScript. Every value is compatible with any, and any allows you to access every property or call every method. any effectively turns off type-checking, giving you some room to breathe during your migration process.
或者,你可以用 注释你的参数unknown。这也允许你将所有内容传递给函数,但在你了解更多类型之前,不允许你对其执行任何操作。
Alternatively, you can annotate your parameters with unknown. This also allows you to pass everything to a function but won’t allow you to do anything with it until you know more about the type.
@ts-ignore您还可以通过在要排除类型检查的行前添加注释来决定忽略错误。@ts-nocheck文件开头的注释将完全关闭此特定模块的类型检查。
You can also decide to ignore errors by adding a @ts-ignore comment before the line you want to exclude from type-checking. A @ts-nocheck comment at the beginning of your file turns off type-checking entirely for this particular module.
对于迁移来说,一个非常棒的注释指令是@ts-expect-error。它的工作原理是@ts-ignore,它会吞掉类型检查过程中的错误,但如果没有发现类型错误,则会生成红色波浪线。
A comment directive that is fantastic for migration is @ts-expect-error. It works like @ts-ignore as it will swallow errors from the type-checking progress but will produce red squiggly lines if no type error is found.
迁移时,这可以帮助您找到已成功移动到 TypeScript 的位置。当没有@ts-expect-error剩余指令时,您就完成了:
When migrating, this helps you find the spots that you successfully moved to TypeScript. When there are no @ts-expect-error directives left, you’re done:
functionprintPerson(person:Person){console.log(person.name);}// This error will be swallowed// @ts-expect-errorprintPerson(123);functionprintNumber(nr:number){console.log(nr);}// v- Unused '@ts-expect-error' directive.ts(2578)// @ts-expect-errorprintNumber(123);
functionprintPerson(person:Person){console.log(person.name);}// This error will be swallowed// @ts-expect-errorprintPerson(123);functionprintNumber(nr:number){console.log(nr);}// v- Unused '@ts-expect-error' directive.ts(2578)// @ts-expect-errorprintNumber(123);
这种技术的优点在于你可以调换职责。通常,你必须确保将正确的值传递给函数;现在你可以确保函数能够处理正确的输入。
The great thing about this technique is that you flip responsibilities. Usually, you have to make sure that you pass in the right values to a function; now you can make sure that the function is able to handle the right input.
在整个迁移过程中消除错误的所有可能性都有一个共同点:它们都是明确的。您需要明确设置@ts-expect-error注释、将函数参数注释为any,或完全忽略文件进行类型检查。这样,您就可以在迁移过程中始终搜索这些逃生舱,并确保随着时间的推移,您已经摆脱了所有错误。
All possibilities for getting rid of errors throughout your migration process have one thing in common: they’re explicit. You need to explicitly set @ts-expect-error comments, annotate function parameters as any, or ignore files entirely from type-checking. With that, you can always search for those escape hatches during the migration process and make sure that, over time, you got rid of them all.
从Definitely Typed安装社区维护的类型定义。
From Definitely Typed, install community-maintained type definitions.
Definitely Typed 是 GitHub 上最大的、最活跃的存储库之一,收集了由社区开发和维护的高质量 TypeScript 类型定义 。
Definitely Typed is one of the biggest and most active repositories on GitHub and collects high-quality TypeScript type definitions developed and maintained by the community.
维护的类型定义数量接近10,000个,并且很少有JavaScript库不可用。
The number of maintained type definitions is close to 10,000, and there is rarely a JavaScript library not available.
所有类型定义都经过了 lint 和检查,并部署到命名空间下的 Node.js 包注册表 NPM 中。NPM 在每个包的信息站点上都有一个指示器,显示是否提供 Definitely Typed 类型定义,如图1-2@types所示。
All type definitions are linted, checked, and deployed to the Node.js package registry NPM under the @types namespace. NPM has an indicator on each package’s information site that shows if Definitely Typed type definitions are available, as you can see in Figure 1-2.
单击此徽标将带您进入类型定义的实际站点。如果某个软件包已经有第一方类型定义,则会在软件包名称旁边显示一个小的 TS 徽标,如图1-3所示。
Clicking on this logo leads you to the actual site for type definitions. If a package has first-party type definitions already available, it shows a small TS logo next to the package name, as shown in Figure 1-3.
例如,要安装流行的 JavaScript 框架 React 的类型,可以将@types/react包安装到本地依赖项中:
To install, for example, typings for the popular JavaScript framework React, you install the @types/react package to your local dependencies:
# Installing React$npminstall--savereact# Installing Type Definitions$npminstall--save-dev@types/react
# Installing React$npminstall--savereact# Installing Type Definitions$npminstall--save-dev@types/react
在这个例子中,我们将类型安装到开发依赖项中,因为我们在开发应用程序时使用它们,并且编译结果无论如何都不会使用这些类型。
In this example we install types to development dependencies, since we consume them while developing the application, and the compiled result has no use of the types anyway.
默认情况下,TypeScript 将拾取它能找到的、位于相对于项目根文件夹的可见@types文件夹中的类型定义。它还将拾取来自node_modules/@types的所有类型定义;请注意,这是 NPM 安装的位置,例如
。@types/react
By default, TypeScript will pick up type definitions it can find that are in visible @types folders relative to your project’s root folder. It will also pick up all type definitions from node_modules/@types; note that this is where NPM installs, for
example, @types/react.
我们这样做是因为tsconfig.jsontypeRoots中的编译器选项设置为和。如果您需要覆盖此设置,请确保包含原始文件夹(如果您想从 Definitely Typed 中获取类型定义):@types./node_modules/@types
We do this because the typeRoots compiler option in tsconfig.json is set to @types and ./node_modules/@types. Should you need to override this setting, make sure to include the original folders if you want to pick up type definitions from Definitely Typed:
{"compilerOptions":{"typeRoots":["./typings","./node_modules/@types"]}}
{"compilerOptions":{"typeRoots":["./typings","./node_modules/@types"]}}
请注意,只需将类型定义安装到node_modules/@types中,TypeScript 就会在编译期间加载它们。这意味着如果某些类型声明了全局变量,TypeScript 就会选择它们。
Note that just by installing type definitions into node_modules/@types, TypeScript will load them during compilation. This means that if some types declare globals, TypeScript will pick them up.
您可能希望通过types在编译器选项的设置中指定哪些包应该被允许对全局范围做出贡献:
You might want to explicitly state which packages should be allowed to contribute to the global scope by specifying them in the types setting in your compiler options:
{"compilerOptions":{"types":["node","jest"]}}
{"compilerOptions":{"types":["node","jest"]}}
请注意,此设置只会影响对全局范围的贡献。如果通过 import 语句加载节点模块,TypeScript 仍将从@types中选择正确的类型:
Note that this setting will only affect the contributions to the global scope. If you load node modules via import statements, TypeScript still will pick up the correct types from @types:
// If `@types/lodash` is installed, we get proper// type defintions for this NPM packageimport_from"lodash"constresult=_.flattenDeep([1,[2,[3,[4]],5]]);
// If `@types/lodash` is installed, we get proper// type defintions for this NPM packageimport_from"lodash"constresult=_.flattenDeep([1,[2,[3,[4]],5]]);
我们将在方案 1.7中再次讨论此设置。
We will revisit this setting in Recipe 1.7.
为每个前端和后端创建两个tsconfig文件,并将共享 依赖项作为复合文件加载。
Create two tsconfig files for each frontend and backend, and load shared dependencies as composites.
Node.js 和浏览器都运行 JavaScript,但它们对开发人员应该如何处理环境的理解截然不同。Node.js 适用于服务器、命令行工具以及所有无需 UI 即可运行的东西——无头。它有自己的一套 API 和标准库。这个小脚本启动一个 HTTP 服务器:
Node.js and the browser both run JavaScript, but they have a very different understanding of what developers should do with the environment. Node.js is meant for servers, command-line tools, and everything that runs without a UI—headless. It has its own set of APIs and standard library. This little script starts an HTTP server:
consthttp=require('http');consthostname='127.0.0.1';constport=process.env.PORT||3000;constserver=http.createServer((req,res)=>{res.statusCode=200;res.setHeader('Content-Type','text/plain');res.end('Hello World');});server.listen(port,hostname,()=>{console.log(`Server running at http://${hostname}:${port}/`);});
consthttp=require('http');consthostname='127.0.0.1';constport=process.env.PORT||3000;constserver=http.createServer((req,res)=>{res.statusCode=200;res.setHeader('Content-Type','text/plain');res.end('Hello World');});server.listen(port,hostname,()=>{console.log(`Server running at http://${hostname}:${port}/`);});
虽然毫无疑问它是 JavaScript,但 Node.js 也有一些独有的东西:
While it’s without a doubt JavaScript, some things are unique to Node.js:
"http"是 Node.js 内置的模块,用于处理与 HTTP 相关的所有内容。它通过 加载require,这是 Node 模块系统CommonJS的一个指标。在 Node.js 中还有其他加载模块的方法,如我们在1.9 节中看到的那样,但最近 CommonJS 是最常见的。
"http" is a built-in Node.js module for everything related to HTTP. It is loaded via require, which is an indicator for Node’s module system called CommonJS. There are other ways to load modules in Node.js as we see in Recipe 1.9, but recently CommonJS has been the most common.
该process对象是一个全局对象,包含有关环境变量和当前 Node.js 进程的一般信息。这也是 Node.js 所独有的。
The process object is a global object containing information on environment variables and the current Node.js process in general. This is also unique to Node.js.
及其函数console几乎在每个 JavaScript 运行时中都可用,但它在 Node 中的作用与在浏览器中的作用不同。在 Node 中,它会在 STDOUT 上打印;在浏览器中,它会将一行打印到开发工具中。
The console and its functions are available in almost every JavaScript runtime, but what it does in Node is different from what it does in the browser. In Node, it prints on STDOUT; in the browser, it will print a line to the development tools.
当然,Node.js 还有更多独特的 API。但浏览器中的 JavaScript 也是如此:
There are of course many more unique APIs for Node.js. But the same goes for JavaScript in the browser:
import{msg}from`./msg.js`;document.querySelector('button')?.addEventListener("click",()=>{console.log(msg);});
import{msg}from`./msg.js`;document.querySelector('button')?.addEventListener("click",()=>{console.log(msg);});
多年来,ECMAScript 模块一直没有办法加载模块,现在终于进入了 JavaScript 和浏览器。这行代码从另一个 JavaScript 模块加载一个对象。它以原生方式在浏览器中运行,是 Node.js 的第二个模块系统(参见方案 1.9)。
After years without a way to load modules, ECMAScript modules have found their way into JavaScript and the browsers. This line loads an object from another JavaScript module. This runs in the browser natively and is a second module system for Node.js (see Recipe 1.9).
浏览器中的 JavaScript 旨在与 UI 事件交互。指向文档对象模型 (DOM)document中的元素的对象和概念是浏览器独有的。添加事件侦听器并侦听“点击”事件也是如此。Node.js 中没有这个。querySelector
JavaScript in the browser is meant to interact with UI events. The document object and the idea of a querySelector that points to elements in the Document Object Model (DOM) are unique to the browser. So is adding an event listener and listening on “click” events. You don’t have this in Node.js.
再说一遍console,它具有与 Node.js 相同的 API,但结果略有
不同。
And again, console. It has the same API as in Node.js, but the result is a bit
different.
差异如此之大,很难创建一个可以同时处理这两个部分的 TypeScript 项目。如果您正在编写全栈应用程序,则需要创建两个 TypeScript 配置文件来处理堆栈的每个部分。
The differences are so big, it’s hard to create one TypeScript project that handles both. If you are writing a full-stack application, you need to create two TypeScript configuration files that deal with each part of your stack.
我们先来处理后端。假设你想用 Node.js 编写一个 Express.js 服务器(Express 是 Node 的一个流行服务器框架)。首先,创建一个新的 NPM 项目,如方案 1.1所示。然后,安装 Express 作为依赖项:
Let’s work on the backend first. Let’s assume you want to write an Express.js server in Node.js (Express is a popular server framework for Node). First, you create a new NPM project as shown in Recipe 1.1. Then, install Express as a dependency:
$npminstall--saveexpress
$npminstall--saveexpress
并从 Definitely Typed 安装 Node.js 和 Express 的类型定义:
And install type definitions for Node.js and Express from Definitely Typed:
$npminstall-D@types/express@types/node
$npminstall-D@types/express@types/node
创建一个名为server 的新文件夹。这是您的 Node.js 代码所在的位置。不要通过 创建新的tsconfig.json ,而是在项目的servertsc文件夹中创建新的tsconfig.json。以下是内容:
Create a new folder called server. This is where your Node.js code goes. Instead of creating a new tsconfig.json via tsc, create a new tsconfig.json in your project’s server folder. Here are the contents:
// server/tsconfig.json{"compilerOptions":{"target":"ESNext","lib":["ESNext"],"module":"commonjs","rootDir":"./","moduleResolution":"node","types":["node"],"outDir":"../dist/server","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"skipLibCheck":true}}
// server/tsconfig.json{"compilerOptions":{"target":"ESNext","lib":["ESNext"],"module":"commonjs","rootDir":"./","moduleResolution":"node","types":["node"],"outDir":"../dist/server","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"skipLibCheck":true}}
你应该已经了解了很多,但有几件事特别突出:
You should already know a lot of this, but a few things stand out:
该module属性设置为commonjs,即原始 Node.js 模块系统。所有import和export语句都将转换为其 CommonJS 对应项。
The module property is set to commonjs, the original Node.js module system. All import and export statements will be transpiled to their CommonJS counterpart.
属性types设置为["node"]。此属性包括您希望全局可用的所有库。如果"node"处于全局范围内,您将获得require、process和全局空间中的其他 Node.js 细节的类型信息。
The types property is set to ["node"]. This property includes all the libraries you want to have globally available. If "node" is in the global scope, you will get type information for require, process, and other Node.js specifics that are in the global space.
要编译服务器端代码,请运行:
To compile your server-side code, run:
$npxtsc-p服务器/tsconfig.json
$npxtsc-pserver/tsconfig.json
现在对于客户端来说:
Now for the client:
// client/tsconfig.json{"compilerOptions":{"target":"ESNext","lib":["DOM","ESNext"],"module":"ESNext","rootDir":"./","moduleResolution":"node","types":[],"outDir":"../dist/client","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"skipLibCheck":true}}
// client/tsconfig.json{"compilerOptions":{"target":"ESNext","lib":["DOM","ESNext"],"module":"ESNext","rootDir":"./","moduleResolution":"node","types":[],"outDir":"../dist/client","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"skipLibCheck":true}}
有一些相似之处,但同样,有几点很突出:
There are some similarities, but again, a few things stand out:
您添加DOM到lib属性中。这为您提供了与浏览器相关的所有内容的类型定义。您需要通过 Definitely Typed 安装 Node.js 类型,而 TypeScript 会随编译器一起提供浏览器的最新类型定义。
You add DOM to the lib property. This gives you type definitions for everything related to the browser. Where you needed to install Node.js typings via Definitely Typed, TypeScript ships the most recent type definitions for the browser with the compiler.
数组types为空。这将从我们的全局类型中删除 "node"。由于您只能为每个package.json安装类型定义,因此"node"我们之前安装的类型定义将在整个代码库中可用。
client但是,对于部分,您想摆脱它们。
The types array is empty. This will remove "node" from our global typings. Since you only can install type definitions per package.json, the "node" type definitions we installed earlier would be available in the entire code base. For the
client part, however, you want to get rid of them.
要编译前端代码,请运行:
To compile your frontend code, run:
$npxtsc-p客户端/tsconfig.json
$npxtsc-pclient/tsconfig.json
请注意,您配置了两个不同的tsconfig.json文件。Visual Studio Code 等编辑器仅获取每个文件夹中tsconfig.json文件的配置信息。您也可以将它们命名为tsconfig.server.json和tsconfig.client.json,并将它们放在项目的根文件夹中(并调整所有目录属性)。tsc将使用正确的配置并在发现任何错误时抛出错误,但编辑器将主要保持静默或使用默认配置。
Please note that you configured two distinct tsconfig.json files. Editors like Visual Studio Code pick up configuration information only for tsconfig.json files per folder. You could as well name them tsconfig.server.json and tsconfig.client.json and have them in your project’s root folder (and adjust all directory properties). tsc will use the correct configurations and throw errors if it finds any, but the editor will mostly stay silent or work with a default configuration.
如果您想要共享依赖项,事情会变得有点复杂。实现共享依赖项的一种方法是使用项目引用和复合项目。这意味着您将共享代码提取到其自己的文件夹中,但告诉 TypeScript,这将是另一个项目的依赖项。
Things get a bit hairier if you want to have shared dependencies. One way to achieve shared dependencies is to use project references and composite projects. This means that you extract your shared code in its own folder, but tell TypeScript that this is meant to be a dependency project of another one.
在与客户端和服务端同级目录下创建一个共享文件夹,在共享文件夹中创建一个tsconfig.json 文件,内容如下:
Create a shared folder on the same level as client and server. Create a tsconfig.json in shared with these contents:
// shared/tsconfig.json{"compilerOptions":{"composite":true,"target":"ESNext","module":"ESNext","rootDir":"../shared/","moduleResolution":"Node","types":[],"declaration":true,"outDir":"../dist/shared","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"skipLibCheck":true},}
// shared/tsconfig.json{"compilerOptions":{"composite":true,"target":"ESNext","module":"ESNext","rootDir":"../shared/","moduleResolution":"Node","types":[],"declaration":true,"outDir":"../dist/shared","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"skipLibCheck":true},}
有两件事再次引人注目:
Two things stand out again:
标志composite设置为true。这允许其他项目引用该项目。
The flag composite is set to true. This allows other projects to reference this one.
该declaration标志也设置为true。这将从您的代码生成d.ts文件,以便其他项目可以使用类型信息。
The declaration flag is also set to true. This will generate d.ts files from your code so other projects can consume type information.
要将它们包含在客户端和服务器代码中,请将这一行添加到client/tsconfig.json和server/tsconfig.json中:
To include them in your client and server code, add this line to client/tsconfig.json and server/tsconfig.json:
// server/tsconfig.json// client/tsconfig.json{"compilerOptions":{// Same as before},"references":[{"path":"../shared/tsconfig.json"}]}
// server/tsconfig.json// client/tsconfig.json{"compilerOptions":{// Same as before},"references":[{"path":"../shared/tsconfig.json"}]}
一切就绪。您可以编写共享依赖项并将其包含在 客户端和服务器代码中。
And you are all set. You can write shared dependencies and include them in your client and server code.
但是,有一个警告。如果您仅共享模型和类型信息,这种方法会很好用,但是一旦您共享实际功能,您就会发现两个不同的模块系统(Node 中的 CommonJS,浏览器中的 ECMAScript 模块)无法统一在一个编译文件中。您要么创建 ESNext 模块,但无法在 CommonJS 代码中导入它,要么创建 CommonJS 代码,但无法在浏览器中导入它。
There is a caveat, however. This works great if you share, for example, only models and type information, but the moment you share actual functionality, you will see that the two different module systems (CommonJS in Node, ECMAScript modules in the browser) can’t be unified in one compiled file. You either create an ESNext module and can’t import it in CommonJS code or create CommonJS code and can’t import it in the browser.
您可以做两件事:
There are two things you can do:
编译为 CommonJS 并让捆绑器负责浏览器的模块解析工作。
Compile to CommonJS and let a bundler take care of the module resolution work for the browser.
编译为 ECMAScript 模块并基于 ECMAScript 模块编写现代 Node.js 应用程序。有关更多信息,请参阅范例 1.9 。
Compile to ECMAScript modules and write modern Node.js applications based on ECMAScript modules. See Recipe 1.9 for more information.
Since you are starting out new, I strongly recommend the second option.
为开发和构建创建单独的tsconfig,并在后者中排除所有测试文件。
Create a separate tsconfig for development and build, and exclude all test files in the latter one.
在 JavaScript 和 Node.js 生态系统中,有很多单元测试框架和测试运行器。它们在细节上各不相同,有不同的观点,或针对特定需求量身定制。其中一些可能比其他的更漂亮。
In the JavaScript and Node.js ecosystem, there are a lot of unit testing frameworks and test runners. They vary in detail, have different opinions, or are tailored for certain needs. Some of them might just be prettier than others.
虽然像Ava这样的测试运行器依赖于导入模块来将框架纳入范围,但其他测试运行器提供了一组全局变量。以Mocha为例:
While test runners like Ava rely on importing modules to get the framework into scope, others provide a set of globals. Take Mocha, for example:
importassertfrom"assert";import{add}from"..";describe("Adding numbers",()=>{it("should add two numbers",()=>{assert.equal(add(2,3),5);});});
importassertfrom"assert";import{add}from"..";describe("Adding numbers",()=>{it("should add two numbers",()=>{assert.equal(add(2,3),5);});});
assert来自 Node.js 内置断言库,但describe、it和更多都是 Mocha 提供的全局变量。它们也仅在 Mocha CLI 运行时存在。
assert comes from the Node.js built-in assertion library, but describe, it, and many more are globals provided by Mocha. They also only exist when the Mocha CLI is running.
这对您的类型设置带来了一些挑战,因为这些功能对于编写测试是必要的,但在您执行实际应用程序时不可用。
This provides a bit of a challenge for your type setup, as those functions are necessary to write tests but aren’t available when you execute your actual application.
解决方案是创建两个不同的配置文件:一个常规的tsconfig.json,用于开发,你的编辑器可以选择它(记住配方 1.6),还有一个单独的tsconfig.build.json,当你想编译你的应用程序时使用。
The solution is to create two different configuration files: a regular tsconfig.json for development that your editor can pick up (remember Recipe 1.6) and a separate tsconfig.build.json that you use when you want to compile your application.
第一个包含您需要的所有全局变量,包括 Mocha 的类型;后者确保您的编译中不包含任何测试文件。
The first one includes all the globals you need, including types for Mocha; the latter makes sure no test file is included within your compilation.
让我们一步一步地进行。我们以 Mocha 为例,但其他提供全局变量的测试运行器(如Jest)的工作方式也相同。
Let’s go through this step by step. We look at Mocha as an example, but other test runners that provide globals like Jest work just the same way.
首先,安装 Mocha 及其类型:
First, install Mocha and its types:
$npminstall--save-devmocha@types/mocha@types/node
$npminstall--save-devmocha@types/mocha@types/node
创建一个新的tsconfig.base.json。由于开发和构建之间的唯一区别在于要包含的文件集和激活的库,因此您希望将所有其他编译器设置放在一个文件中,以便两者重复使用。Node.js 应用程序的示例文件如下所示:
Create a new tsconfig.base.json. Since the only differences between development and build are the set of files to be included and the libraries activated, you want to have all the other compiler settings located in one file you can reuse for both. An example file for a Node.js application would look like this:
// tsconfig.base.json{"compilerOptions":{"target":"esnext","module":"commonjs","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"outDir":"./dist","skipLibCheck":true}}
// tsconfig.base.json{"compilerOptions":{"target":"esnext","module":"commonjs","esModuleInterop":true,"forceConsistentCasingInFileNames":true,"strict":true,"outDir":"./dist","skipLibCheck":true}}
源文件应位于 src中;测试文件应位于相邻的文件夹test中。本指南中创建的设置还允许您在项目中的任何位置创建以.test.ts结尾的文件。
The source files should be located in src; test files should be located in an adjacent folder test. The setup you create in this recipe will also allow you to create files ending with .test.ts anywhere in your project.
使用基本开发配置创建一个新的tsconfig.json。此配置用于编辑器反馈和使用 Mocha 运行测试。您可以从tsconfig.base.json扩展基本设置,并告知 TypeScript 要选择哪些文件夹 进行编译:
Create a new tsconfig.json with your base development configuration. This one is used for editor feedback and for running tests with Mocha. You extend the basic settings from tsconfig.base.json and inform TypeScript which folders to pick up for compilation:
// tsconfig.json{"extends":"./tsconfig.base.json","compilerOptions":{"types":["node","mocha"],"rootDirs":["test","src"]}}
// tsconfig.json{"extends":"./tsconfig.base.json","compilerOptions":{"types":["node","mocha"],"rootDirs":["test","src"]}}
请注意,您添加了typesNode 和 Mocha。该types属性定义哪些全局变量可用,并且在开发设置中,您同时拥有这两个全局变量。
Note that you add types for Node and Mocha. The types property defines which globals are available and, in the development setting, you have both.
此外,您可能会发现在执行测试之前编译测试很麻烦。有一些快捷方式可以帮助您。例如,ts-node运行本地安装的 Node.js 并首先进行内存中 TypeScript 编译:
Additionally, you might find that compiling your tests before executing them is cumbersome. There are shortcuts to help you. For example, ts-node runs your local installation of Node.js and does an in-memory TypeScript compilation first:
$npminstall--save-devts-node$npxmocha-rts-node/registertests/*.ts
$npminstall--save-devts-node$npxmocha-rts-node/registertests/*.ts
设置好开发环境后,就到了构建环境。创建一个tsconfig.build.json。它看起来与tsconfig.json类似,但你会立即发现差异:
With the development environment set up, it’s time for the build environment. Create a tsconfig.build.json. It looks similar to tsconfig.json, but you will spot the difference right away:
// tsconfig.build.json{"extends":"./tsconfig.base.json","compilerOptions":{"types":["node"],"rootDirs":["src"]},"exclude":["**/*.test.ts","**/test/**"]}
// tsconfig.build.json{"extends":"./tsconfig.base.json","compilerOptions":{"types":["node"],"rootDirs":["src"]},"exclude":["**/*.test.ts","**/test/**"]}
除了更改types和之外rootDirs,您还可以定义要从类型检查和编译中排除哪些文件。您可以使用通配符模式来排除位于测试文件夹中以.test.ts结尾的所有文件。根据您的喜好,您还可以将.spec.ts或spec文件夹添加到此数组中。
In addition to changing types and rootDirs, you define which files to exclude from type-checking and compilation. You use wild-card patterns that exclude all files ending with .test.ts that are located in test folders. Depending on your taste, you can also add .spec.ts or spec folders to this array.
通过引用正确的 JSON 文件来编译您的项目:
Compile your project by referring to the right JSON file:
$npxtsc-ptsconfig.build.json
$npxtsc-ptsconfig.build.json
您将看到在结果文件(位于dist)中看不到任何测试文件。此外,虽然您仍然可以访问describe和it编辑源文件,但如果您尝试编译,则会收到错误:
You will see that in the result files (located in dist), you won’t see any test file. Also, while you still can access describe and it when editing your source files, you will get an error if you try to compile:
$ npx tsc -p tsconfig.build.json
src/index.ts:5:1 - 错误 TS2593:找不到名称“describe”。
您需要为测试运行器安装类型定义吗?
尝试 `npm i --save-dev @types/jest` 或 `npm i --save-dev @types/mocha`
然后将“jest”或“mocha”添加到 tsconfig 中的类型字段。
5 describe("这不起作用", () => {})
~~~~~~~~
在 src/index.ts 中发现 1 个错误:5$ npx tsc -p tsconfig.build.json
src/index.ts:5:1 - error TS2593: Cannot find name 'describe'.
Do you need to install type definitions for a test runner?
Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`
and then add 'jest' or 'mocha' to the types field in your tsconfig.
5 describe("this does not work", () => {})
~~~~~~~~
Found 1 error in src/index.ts:5
如果你不喜欢在开发模式下污染你的全局变量,你可以选择与方案 1.6中类似的设置,但它不允许你在源文件旁边编写测试。
If you don’t like polluting your globals during development mode, you can choose a similar setup as in Recipe 1.6, but it won’t allow you to write tests adjacent to your source files.
Finally, you can always opt for a test runner that prefers the module system.
target在tsconfig的编译器选项module中设置并指向带有.js扩展名的模块。此外,通过 NPM 将类型安装到依赖项,并使用tsconfig中的属性告诉 TypeScript 在哪里查找类型:esnextpath
Set target and module in your tsconfig’s compiler options to esnext and point to your modules with a .js extension. In addition, install types to dependencies via NPM, and use the path property in your tsconfig to tell TypeScript where to look for types:
// tsconfig.json{"compilerOptions":{"target":"esnext","module":"esnext","paths":{"https://esm.sh/lodash@4.17.21":["node_modules/@types/lodash/index.d.ts"]}}}
// tsconfig.json{"compilerOptions":{"target":"esnext","module":"esnext","paths":{"https://esm.sh/lodash@4.17.21":["node_modules/@types/lodash/index.d.ts"]}}}
现代浏览器支持开箱即用的模块加载。您无需将应用打包成一组较小的文件,而是可以直接使用原始 JavaScript 文件。
Modern browsers support module loading out of the box. Instead of bundling your app into a smaller set of files, you can use the raw JavaScript files directly.
内容分发网络 (CDN)(例如esm.sh、unpkg等)旨在将节点模块和 JavaScript 依赖项作为 URL 分发,以供本机 ECMAScript 模块加载使用。
Content Delivery Networks (CDNs) like esm.sh, unpkg, and others are designed to distribute node modules and JavaScript dependencies as URLs, consumable by native ECMAScript module loading.
通过适当的缓存和最先进的 HTTP,ECMAScript 模块成为应用程序的真正替代品。
With proper caching and state-of-the-art HTTP, ECMAScript modules become a real alternative for apps.
TypeScript 不包含现代打包器,因此无论如何你都需要安装一个额外的工具。但如果你决定先使用模块,那么在使用 TypeScript 时需要考虑一些事项。
TypeScript does not include a modern bundler, so you would need to install an extra tool anyway. But if you decide to go module first, there are a few things to consider when working with TypeScript.
您想要实现的是用 TypeScript 编写import和export语句,但保留模块加载语法并让浏览器处理模块解析:
What you want to achieve is to write import and export statements in TypeScript but preserve the module-loading syntax and let the browser handle module resolution:
// File module.tsexportconstobj={name:"Stefan",};// File index.tsimport{obj}from"./module";console.log(obj.name);
// File module.tsexportconstobj={name:"Stefan",};// File index.tsimport{obj}from"./module";console.log(obj.name);
为了实现这一点,告诉 TypeScript:
To achieve this, tell TypeScript to:
编译为可以理解模块的 ECMAScript 版本
Compile to an ECMAScript version that understands modules
使用 ECMAScript 模块语法进行模块代码生成
Use the ECMAScript module syntax for module code generation
更新tsconfig.json中的两个属性:
Update two properties in your tsconfig.json:
// tsconfig.json{"compilerOptions":{"target":"esnext","module":"esnext"}}
// tsconfig.json{"compilerOptions":{"target":"esnext","module":"esnext"}}
module告诉 TypeScript 如何转换 import 和 export 语句。默认将模块加载转换为 CommonJS,如方案 1.2所示。设置module为esnext将使用 ECMAScript 模块加载,从而保留语法。
module tells TypeScript how to transform import and export statements. The default converts module loading to CommonJS, as seen in Recipe 1.2. Setting module to esnext will use ECMAScript module loading and thus preserve the syntax.
target告诉 TypeScript 您想要将代码转译到的 ECMAScript 版本。每年都会发布一个包含新功能的新 ECMAScript 版本。设置target为esnext将始终以最新的 ECMAScript 版本为目标。
target tells TypeScript the ECMAScript version you want to transpile your code to. Once a year, there’s a new ECMAScript release with new features. Setting target to esnext will always target the latest ECMAScript version.
根据您的兼容性目标,您可能希望将此属性设置为与您想要支持的浏览器兼容的 ECMAScript 版本。这通常是带有年份的版本(例如es2015,,等)。ECMAScript 模块适用于从 1999 年起的每个版本。es2016如果您使用较旧的版本,您将无法在浏览器中本地加载 ECMAScript 模块。es2017es2015
Depending on your compatibility goals, you might want to set this property to the ECMAScript version compatible with the browsers you want to support. This is usually a version with a year (e.g. es2015, es2016, es2017, etc). ECMAScript modules work with every version from es2015 on. If you go for an older version, you won’t be able to load ECMAScript modules natively in the browser.
更改这些编译器选项已经做了一件重要的事情:它保持语法不变。一旦您想要运行代码,就会出现问题。
Changing these compiler options already does one important thing: it leaves the syntax intact. A problem occurs once you want to run your code.
通常,TypeScript 中的 import 语句指向没有扩展名的文件。你写import { obj } from "./module",省略.ts。编译后,仍然缺少此扩展名。但浏览器需要一个扩展名才能真正指向相应的 JavaScript 文件。
Usually, import statements in TypeScript point to files without an extension. You write import { obj } from "./module", leaving out .ts. Once you compile, this extension is still missing. But the browser needs an extension to actually point to the respective JavaScript file.
解决方案:添加.js扩展名,即使你在开发时指向.ts文件。TypeScript 足够聪明,可以识别这一点:
The solution: Add a .js extension, even though you are pointing to a .ts file when you develop. TypeScript is smart enough to pick that up:
// index.ts// This still loads types from 'module.ts', but keeps// the reference intact once we compile it.import{obj}from'./module.js';console.log(obj.name);
// index.ts// This still loads types from 'module.ts', but keeps// the reference intact once we compile it.import{obj}from'./module.js';console.log(obj.name);
对于您的项目模块,这就是您所需要的全部!
For your project’s modules, that’s all you need!
当你想使用依赖项时,它会变得更加有趣。如果你使用原生应用,你可能想要从 CDN 加载模块,比如esm.sh:
It gets a lot more interesting when you want to use dependencies. If you go native, you might want to load modules from a CDN, like esm.sh:
import_from"https://esm.sh/lodash@4.17.21"// ^- Error 2307constresult=_.flattenDeep([1,[2,[3,[4]],5]]);console.log(result);
import_from"https://esm.sh/lodash@4.17.21"// ^- Error 2307constresult=_.flattenDeep([1,[2,[3,[4]],5]]);console.log(result);
TypeScript 将出现以下错误信息:“找不到模块… 或其对应的类型声明。(2307)”
TypeScript will error with the following message: “Cannot find module … or its corresponding type declarations. (2307)”
当文件位于磁盘上而不是通过 HTTP 位于服务器上时,TypeScript 的模块解析会起作用。为了获取我们需要的信息,我们必须为 TypeScript 提供自己的解析。
TypeScript’s module resolution works when files are on your disk, not on a server via HTTP. To get the info we need, we have to provide TypeScript with a resolution of our own.
尽管我们从 URL 加载依赖项,但这些依赖项的类型信息仍存在于 NPM 中。对于lodash,您可以从 Definitely Typed 安装类型信息:
Even though we are loading dependencies from URLs, the type information for these dependencies lives with NPM. For lodash, you can install type information from Definitely Typed:
$npminstall-D@types/lodash
$npminstall-D@types/lodash
对于自带类型的依赖项,你可以直接安装依赖项:
For dependencies that come with their own types, you can install the dependencies directly:
$npminstall-Dpreact
$npminstall-Dpreact
安装类型后,使用path编译器选项中的属性来告诉 TypeScript 如何解析你的 URL:
Once the types are installed, use the path property in your compiler options to tell TypeScript how to resolve your URL:
// tsconfig.json{"compilerOptions":{// ..."paths":{"https://esm.sh/lodash@4.17.21":["node_modules/@types/lodash/index.d.ts"]}}}
// tsconfig.json{"compilerOptions":{// ..."paths":{"https://esm.sh/lodash@4.17.21":["node_modules/@types/lodash/index.d.ts"]}}}
请务必指向正确的文件!
Be sure to point to the right file!
如果您不想使用类型,或者只是找不到类型,那么还有一个逃生舱。在 TypeScript 中,我们可以使用any来有意禁用类型检查。对于模块,我们可以做一些非常类似的事情——忽略 TypeScript 错误:
There’s also an escape hatch if you don’t want to use typings, or if you just can’t find typings. Within TypeScript, we can use any to intentionally disable type-checking. For modules, we can do something very similar—ignore the TypeScript error:
// @ts-ignoreimport_from"https://esm.sh/lodash@4.17.21"
// @ts-ignoreimport_from"https://esm.sh/lodash@4.17.21"
ts-ignore删除类型检查中的下一行,并且可以在您想要忽略类型错误的地方使用(参见配方 1.4)。这实际上意味着您将无法获得依赖项的任何类型信息,并且可能会遇到错误,但对于您只需要但找不到任何类型的未维护的旧依赖项,这可能是最终的解决方案。
ts-ignore removes the next line from type-checking and can be used everywhere you want to ignore type errors (see Recipe 1.4). This effectively means that you won’t get any type information for your dependencies and you might run into errors, but it might be the ultimate solution for unmaintained, old dependencies that you just need but won’t find any types for.
将 TypeScript 的模块分辨率设置为"nodeNext"并将文件命名为.mts或.cts。
Set TypeScript’s module resolution to "nodeNext" and name your files .mts or .cts.
随着 Node.js 的出现,CommonJS 模块系统已经成为 JavaScript 生态系统中最流行的模块系统之一。
With the advent of Node.js, the CommonJS module system has become one of the most popular module systems in the JavaScript ecosystem.
这个想法简单而有效:在一个模块中定义导出,并 在另一个模块中需要它们:
The idea is simple and effective: define exports in one module and require them in another:
// person.jsfunctionprintPerson(person){console.log(person.name);}exports={printPerson,};// index.jsconstperson=require("./person");person.printPerson({name:"Stefan",age:40});
// person.jsfunctionprintPerson(person){console.log(person.name);}exports={printPerson,};// index.jsconstperson=require("./person");person.printPerson({name:"Stefan",age:40});
该系统对 ECMAScript 模块产生了巨大影响,并且已成为 TypeScript 模块解析和转译器的默认系统。如果您查看示例 1-1中的 ECMAScript 模块语法,您会发现关键字允许不同的转译。这意味着,使用commonjs模块设置,您的importandexport语句将被转译为requireand exports。
This system has been a huge influence on ECMAScript modules and also has been the default for TypeScript’s module resolution and transpiler. If you look at the ECMAScript modules syntax in Example 1-1, you can see that the keywords allow for different transpilations. This means that with the commonjs module setting, your import and export statements are transpiled to require and exports.
// person.tstypePerson={name:string;age:number;};exportfunctionprintPerson(person){console.log(person.name);}// index.tsimport*aspersonfrom"./person";person.printPerson({name:"Stefan",age:40});
// person.tstypePerson={name:string;age:number;};exportfunctionprintPerson(person){console.log(person.name);}// index.tsimport*aspersonfrom"./person";person.printPerson({name:"Stefan",age:40});
随着 ECMAScript 模块的稳定,Node.js 也开始采用它们。尽管两个模块系统的基础看起来非常相似,但在细节上还是存在一些差异,例如处理默认导出或异步加载 ECMAScript 模块。
With ECMAScript modules stabilizing, Node.js has also started to adopt them. Even though the basics of both module systems seem to be very similar, there are some differences in the details, such as handling default exports or loading ECMAScript modules asynchronously.
由于无法将两个模块系统视为相同但语法不同的系统,Node.js 维护者决定为这两个系统留出空间,并分配不同的文件后缀以指示首选的模块类型。表 1-1显示了不同的后缀、它们在 TypeScript 中的命名方式、TypeScript 将它们编译成什么以及它们可以导入什么。由于 CommonJS 互操作性,可以从 ECMAScript 模块导入 CommonJS 模块,但反过来不行。
As there is no way to treat both module systems the same but with different syntax, the Node.js maintainers decided to give both systems room and assigned different file endings to indicate the preferred module type. Table 1-1 shows the different endings, how they’re named in TypeScript, what TypeScript compiles them to, and what they can import. Thanks to the CommonJS interoperability, it’s fine to import CommonJS modules from ECMAScript modules, but not the other way around.
| 结束 | TypeScript | 编译为 | 可以导入 |
|---|---|---|---|
.js .js |
.ts .ts |
CommonJS CommonJS |
.js格式 .js, .cjs |
.cjs .cjs |
.cts .cts |
CommonJS CommonJS |
.js格式 .js, .cjs |
.mjs .mjs |
.mts .mts |
ES 模块 ES Modules |
.js , .cjs , .mjs .js, .cjs, .mjs |
在 NPM 上发布库的开发人员会在他们的package.json文件中获取额外信息,以指示包的主要类型(module或commonjs),并指向主文件或后备文件列表,以便模块加载器可以选择正确的文件:
Library developers who publish on NPM get extra information in their package.json file to indicate the main type of a package (module or commonjs), and to point to a list of main files or fallbacks so module loaders can pick up the right file:
// package.json{"name":"dependency","type":"module","exports":{".":{// Entry-point for `import "dependency"` in ES Modules"import":"./esm/index.js",// Entry-point for `require("dependency") in CommonJS"require":"./commonjs/index.cjs",},},// CommonJS Fallback"main":"./commonjs/index.cjs"}
// package.json{"name":"dependency","type":"module","exports":{".":{// Entry-point for `import "dependency"` in ES Modules"import":"./esm/index.js",// Entry-point for `require("dependency") in CommonJS"require":"./commonjs/index.cjs",},},// CommonJS Fallback"main":"./commonjs/index.cjs"}
在 TypeScript 中,你主要编写 ECMAScript 模块语法,并让编译器决定最终要创建哪种模块格式。现在可能有两种:CommonJS 和 ECMAScript 模块。
In TypeScript, you write mainly ECMAScript module syntax and let the compiler decide which module format to create in the end. Now there are possibly two: CommonJS and ECMAScript modules.
为了同时允许两者,您可以在tsconfig.json中将模块分辨率设置为NodeNext:
To allow for both, you can set module resolution in your tsconfig.json to NodeNext:
{"compilerOptions":{"module":"NodeNext"// ...}}
{"compilerOptions":{"module":"NodeNext"// ...}}
使用该标志,TypeScript 将根据依赖项 package.json中的描述选择正确的模块,识别.mts和.cts结尾,并按照表 1-1进行模块导入。
With that flag, TypeScript will pick up the right modules as described in your dependencies package.json, will recognize .mts and .cts endings, and will follow Table 1-1 for module imports.
对于开发人员来说,导入文件时会有所不同。由于 CommonJS 在导入时不需要结尾,因此 TypeScript 仍然支持没有结尾的导入。如果您只使用 CommonJS,则示例 1-1中的示例仍然有效。
For you as a developer, there are differences in importing files. Since CommonJS didn’t require endings when importing, TypeScript still supports imports without endings. The example in Example 1-1 still works, if all you use is CommonJS.
像在方案 1.8中一样,使用文件扩展名导入可以将模块导入 ECMAScript 模块和 CommonJS 模块中:
Importing with file endings, just like in Recipe 1.8, allows modules to be imported in both ECMAScript modules and CommonJS modules:
// index.mtsimport*aspersonfrom"./person.js";// works in bothperson.printPerson({name:"Stefan",age:40});
// index.mtsimport*aspersonfrom"./person.js";// works in bothperson.printPerson({name:"Stefan",age:40});
如果 CommonJS 互操作性不起作用,您可以随时使用语句require。将"node"全局类型添加到您的编译器选项中:
Should CommonJS interoperability not work, you can always fall back on a require statement. Add "node" as global types to your compiler options:
// tsconfig.json{"compilerOptions":{"module":"NodeNext","types":["node"],}}
// tsconfig.json{"compilerOptions":{"module":"NodeNext","types":["node"],}}
然后,使用此 TypeScript 特定的语法导入:
Then, import with this TypeScript-specific syntax:
// index.mtsimportperson=require("./person.cjs");person.printPerson({name:"Stefan",age:40});
// index.mtsimportperson=require("./person.cjs");person.printPerson({name:"Stefan",age:40});
在 CommonJS 模块中,这只是另一个require调用;在 ECMAScript 模块中,这将包括 Node.js 辅助函数:
In a CommonJS module, this will be just another require call; in ECMAScript modules, this will include Node.js helper functions:
// compiled index.mtsimport{createRequireas_createRequire}from"module";const__require=_createRequire(import.meta.url);constperson=__require("./person.cjs");person.printPerson({name:"Stefan",age:40});
// compiled index.mtsimport{createRequireas_createRequire}from"module";const__require=_createRequire(import.meta.url);constperson=__require("./person.cjs");person.printPerson({name:"Stefan",age:40});
请注意,这会降低与浏览器等非 Node.js 环境的兼容性,但最终可能会解决互操作性问题。
Note that this will reduce compatibility with non-Node.js environments like the browser, but it might eventually fix interoperability issues.
这很简单;TypeScript 是内置的。
That’s easy; TypeScript is built in.
Deno 是一个现代 JavaScript 运行时,由开发 Node.js 的同一批人创建。Deno 在许多方面与 Node.js 相似,但也有显著差异:
Deno is a modern JavaScript runtime created by the same people who developed Node.js. Deno is similar to Node.js in many ways, but with significant differences:
Deno 采用 Web 平台标准作为其主要 API,这意味着您将更容易将代码从浏览器移植到服务器。
Deno adopts web platform standards for their main APIs, meaning that you will find it easier to port code from the browser to the server.
仅当您明确激活它时,它才允许文件系统或网络访问。
It allows file system or network access only if you explicitly activate it.
它不通过集中式注册表处理依赖关系,而是再次采用浏览器功能通过 URL 来处理。
It doesn’t handle dependencies via a centralized registry, but—again adopting browser features—via URLs.
哦,它还带有内置开发工具和 TypeScript!
Oh, and it comes with built-in development tooling and TypeScript!
如果你想尝试 TypeScript,Deno 是门槛最低的工具。无需下载任何其他工具(tsc编译器已内置),无需 TypeScript 配置。你只需编写.ts文件,Deno 会处理其余的事情:
Deno is the tool with the lowest barrier if you want to try TypeScript. No need to download any other tool (the tsc compiler is already built in), no need for TypeScript configurations. You write .ts files, and Deno handles the rest:
// main.tsfunctionsayHello(name:string){console.log(`Hello${name}`);}sayHello("Stefan");
// main.tsfunctionsayHello(name:string){console.log(`Hello${name}`);}sayHello("Stefan");
$deno运行main.ts
$denorunmain.ts
Deno 的 TypeScript 可以做所有tsc能做的事情,并且会随着 Deno 的每次更新而更新。但是,当你想配置它时,会有一些不同。
Deno’s TypeScript can do everything tsc can do, and it is updated with every Deno update. However, there are some differences when you want to configure it.
首先,默认配置与 发布的默认配置在默认设置上有差异tsc --init。严格模式功能标志的设置不同,并且它包含对 React 的支持(在服务器端!)。
First, the default configuration has differences in its default settings as opposed to the default configuration issued by tsc --init. Strict mode feature flags are set differently, and it includes support for React (on the server side!).
要更改配置,你应该在根文件夹中创建一个deno.json文件。除非你告诉它不要这样做,否则 Deno 会自动选择它。deno.json包含Deno 运行时的几个配置,包括 TypeScript 编译器选项:
To make changes to the configuration, you should create a deno.json file in your root folder. Deno will automatically pick this up, unless you tell it not to. deno.json includes several configurations for the Deno runtime, including TypeScript compiler options:
{"compilerOptions":{// Your TSC compiler options},"fmt":{// Options for the auto-formatter},"lint":{// Options for the linter}}
{"compilerOptions":{// Your TSC compiler options},"fmt":{// Options for the auto-formatter},"lint":{// Options for the linter}}
您可以在Deno 网站上看到更多可能性。
You can see more possibilities on the Deno website.
默认库也不同。尽管 Deno 支持 Web 平台标准并具有与浏览器兼容的 API,但它需要进行一些删减,因为没有图形用户界面。这就是为什么某些类型(例如 DOM 库)与 Deno 提供的内容发生冲突的原因。
The default libraries are different as well. Even though Deno supports web platform standards and has browser-compatible APIs, it needs to make some cuts because there is no graphical user interface. That’s why some types—for example, the DOM library—clash with what Deno provides.
一些感兴趣的库是:
Some libraries of interest are:
deno.ns,默认的 Deno 命名空间
deno.ns, the default Deno namespace
deno.window,Deno 的全局对象
deno.window, the global object for Deno
deno.worker,Deno 运行时中的 Web Workers 等效项
deno.worker, the equivalent for Web Workers in the Deno runtime
DOM 和子集包含在 Deno 中,但默认情况下未启用。如果你的应用程序同时针对浏览器和 Deno,请将 Deno 配置为包含所有浏览器和 Deno 库:
DOM and subsets are included in Deno, but they are not switched on by default. If your application targets both the browser and Deno, configure Deno to include all browser and Deno libraries:
// deno.json{"compilerOptions":{"target":"esnext","lib":["dom","dom.iterable","dom.asynciterable","deno.ns"]}}
// deno.json{"compilerOptions":{"target":"esnext","lib":["dom","dom.iterable","dom.asynciterable","deno.ns"]}}
Aleph.js是一个针对 Deno 和浏览器的框架的示例。
Aleph.js is an example of a framework that targets both Deno and the browser.
Deno 的另一个不同之处在于依赖项的类型信息是如何分布的。Deno 中的外部依赖项通过 CDN 中的 URL 加载。Deno 本身将其标准库托管在https://deno.land/std上。
Also different with Deno is how type information for dependencies is distributed. External dependencies in Deno are loaded via URLs from a CDN. Deno itself hosts its standard library at https://deno.land/std.
但你也可以使用esm.sh或unpkg之类的 CDN ,如配方 1.8中所示。这些 CDN 通过X-TypeScript-Types在 HTTP 请求中发送标头来分发类型,显示 Deno 将加载类型声明。这也适用于没有第一方类型声明但依赖于
Definitely Typed 的依赖项。
But you can also use CDNs like esm.sh or unpkg, like in Recipe 1.8. These CDNs distribute types by sending an X-TypeScript-Types header with the HTTP request, showing Deno was to load type declarations. This also goes for dependencies that don’t have first-party type declarations but rely on
Definitely Typed.
因此,当您安装依赖项时,Deno 不仅会获取源文件,还会获取所有类型信息。
So the moment you install your dependency, Deno will fetch not only the source files but also all the type information.
如果您不从 CDN 加载依赖项而是在本地加载,则可以在导入依赖项时指向类型声明文件:
If you don’t load a dependency from a CDN but rather have it locally, you can point to a type declaration file the moment you import the dependency:
// @deno-types="./charting.d.ts"import*aschartingfrom"./charting.js";
// @deno-types="./charting.d.ts"import*aschartingfrom"./charting.js";
或者包含对库本身类型的引用:
or include a reference to the typings in the library itself:
// charting.js/// <reference types="./charting.d.ts" />
// charting.js/// <reference types="./charting.d.ts" />
此引用也称为三斜杠指令,是 TypeScript 功能,而不是 Deno 功能。有各种三斜杠指令,主要用于 ECMAScript 之前的模块依赖系统。文档提供了非常好的概述。但是,如果您坚持使用 ECMAScript 模块,则很可能不会使用三斜杠指令。
This reference is also called a triple-slash directive and is a TypeScript feature, not a Deno feature. There are various triple-slash directives, mostly used for pre-ECMAScript module dependency systems. The documentation gives a really good overview. If you stick with ECMAScript modules, you most likely won’t use triple-slash directives, though.
使用tsconfig/bases中的预定义配置并从那里扩展。
Use a predefined configuration from tsconfig/bases and extend from there.
就像 Definitely Typed 托管社区维护的热门库类型定义一样,tsconfig/bases托管一组社区维护的 TypeScript 配置建议,您可以将其用作自己项目的起点。这包括 Ember.js、Svelte 或 Next.js 等框架以及 Node.js 和 Deno 等 JavaScript 运行时。
Just like Definitely Typed hosts community-maintained type definitions for popular libraries, tsconfig/bases hosts a set of community-maintained recommendations for TypeScript configurations you can use as a starting point for your own project. This includes frameworks like Ember.js, Svelte, or Next.js as well as JavaScript runtimes like Node.js and Deno.
配置文件被减少到最低限度,主要处理推荐的库、模块和目标设置,以及一堆适合相应环境的严格模式标志。
The configuration files are reduced to a minimum, dealing mostly with recommended libraries, modules, and target settings, and a bunch of strict mode flags that make sense for the respective environment.
例如,这是针对 Node.js 18 的推荐配置,具有推荐的严格模式设置和 ECMAScript 模块:
For example, this is the recommended configuration for Node.js 18, with a recommended strict mode setting and with ECMAScript modules:
{"$schema":"https://json.schemastore.org/tsconfig","display":"Node 18 + ESM + Strictest","compilerOptions":{"lib":["es2022"],"module":"es2022","target":"es2022","strict":true,"esModuleInterop":true,"skipLibCheck":true,"forceConsistentCasingInFileNames":true,"moduleResolution":"node","allowUnusedLabels":false,"allowUnreachableCode":false,"exactOptionalPropertyTypes":true,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noImplicitReturns":true,"noPropertyAccessFromIndexSignature":true,"noUncheckedIndexedAccess":true,"noUnusedLocals":true,"noUnusedParameters":true,"importsNotUsedAsValues":"error","checkJs":true}}
{"$schema":"https://json.schemastore.org/tsconfig","display":"Node 18 + ESM + Strictest","compilerOptions":{"lib":["es2022"],"module":"es2022","target":"es2022","strict":true,"esModuleInterop":true,"skipLibCheck":true,"forceConsistentCasingInFileNames":true,"moduleResolution":"node","allowUnusedLabels":false,"allowUnreachableCode":false,"exactOptionalPropertyTypes":true,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noImplicitReturns":true,"noPropertyAccessFromIndexSignature":true,"noUncheckedIndexedAccess":true,"noUnusedLocals":true,"noUnusedParameters":true,"importsNotUsedAsValues":"error","checkJs":true}}
要使用此配置,请通过 NPM 安装它:
To use this configuration, install it via NPM:
$npminstall--save-dev@tsconfig/node18-strictest-esm
$npminstall--save-dev@tsconfig/node18-strictest-esm
并将其连接到你自己的 TypeScript 配置中:
and wire it up in your own TypeScript configuration:
{"extends":"@tsconfig/node18-strictest-esm/tsconfig.json","compilerOptions":{// ...}}
{"extends":"@tsconfig/node18-strictest-esm/tsconfig.json","compilerOptions":{// ...}}
这将从预定义配置中获取所有设置。您现在可以开始设置自己的属性,例如根目录和输出目录。
This will pick up all the settings from the predefined configuration. You can now start setting your own properties, for example, root and out directories.
1分配给绑定的对象const仍然可以改变值和属性,从而改变它们的类型。
1 Objects assigned to a const binding can still change values and properties, and thus change their types.
2TypeScript 也可以在其他 JavaScript 运行时上运行,例如 Deno 和浏览器,但它们并非主要目标。
2 TypeScript also works in other JavaScript runtimes, such as Deno and the browser, but they are not intended as main targets.
现在您已完成所有设置,是时候编写一些 TypeScript 了!开始应该很容易,但您很快就会遇到不确定自己是否做对的情况。您应该使用接口还是类型别名?您应该注释还是让类型推断发挥其魔力?那么any和unknown:它们可以安全使用吗?网上有些人说你永远不应该使用它们,那么为什么它们是
TypeScript 的一部分呢?
Now that you are all set up, it’s time to write some TypeScript! Starting out should be easy, but you will soon run into situations where you’re unsure if you’re doing the right thing. Should you use interfaces or type aliases? Should you annotate or let type inference do its magic? What about any and unknown: are they safe to use? Some people on the internet said you should never use them, so why are they part of
TypeScript?
本章将解答所有这些问题。我们将研究构成 TypeScript 的基本类型,并了解经验丰富的 TypeScript 开发人员如何使用它们。您可以将其作为后续章节的基础,这样您就可以了解 TypeScript 编译器如何获取其类型以及如何解释您的 注释。
All these questions will be answered in this chapter. We will look at the basic types that make TypeScript and learn how an experienced TypeScript developer will use them. You can use this as a foundation for the upcoming chapters, so you get a feel for how the TypeScript compiler gets to its types and how it interprets your annotations.
这是关于代码、编辑器和编译器之间的交互。它涉及类型层次结构的上下移动,正如我们将在范例 2.3中看到的那样。无论您是经验丰富的 TypeScript 开发人员还是刚刚入门,您都可以在本章中找到有用的信息。
This is about the interaction between your code, the editor, and the compiler. And it’s about going up and down the type hierarchy, as we will see in Recipe 2.3. Whether you’re an experienced TypeScript developer or just starting out, you’ll find useful information in this chapter.
仅当您想要检查类型时才进行注释。
Annotate only when you want your types checked.
类型注释是一种明确说明预期类型的方法。你知道,在其他编程语言中, 的冗长内容StringBuilder stringBuilder = new StringBuilder()可以确保你真的在处理StringBuilder。与之相反的是类型推断,TypeScript 会尝试为你找出类型:
A type annotation is a way to explicitly tell which types to expect. You know, the prominent stuff in other programming languages, where the verbosity of StringBuilder stringBuilder = new StringBuilder() makes sure that you’re really, really dealing with a StringBuilder. The opposite is type inference, where TypeScript tries to figure out the type for you:
// Type inferenceletaNumber=2;// aNumber: number// Type annotationletanotherNumber:number=3;// anotherNumber: number
// Type inferenceletaNumber=2;// aNumber: number// Type annotationletanotherNumber:number=3;// anotherNumber: number
类型注释也是 TypeScript 和 JavaScript 之间最明显、最明显的语法区别。
Type annotations are also the most obvious and visible syntax difference between TypeScript and JavaScript.
当您开始学习 TypeScript 时,您可能希望对所有内容进行注释以表达您期望的类型。这似乎是显而易见的选择,但您也可以谨慎使用注释,让 TypeScript 为您找出类型。
When you start learning TypeScript, you might want to annotate everything to express the types you’d expect. This might feel like the obvious choice, but you can also use annotations sparingly and let TypeScript figure out types for you.
类型注释是一种表达需要检查契约的方式。如果将类型注释添加到变量声明中,则会告诉编译器在赋值期间检查类型是否匹配:
A type annotation is a way for you to express where contracts have to be checked. If you add a type annotation to a variable declaration, you tell the compiler to check if types match during the assignment:
typePerson={name:string;age:number;};constme:Person=createPerson();
typePerson={name:string;age:number;};constme:Person=createPerson();
如果createPerson返回的内容与 不兼容Person,TypeScript 将抛出错误。如果您确实想确保处理正确的类型,请执行此操作。
If createPerson returns something that isn’t compatible with Person, TypeScript will throw an error. Do this if you really want to be sure you’re dealing with the right type.
此外,从现在起,me是 类型Person,TypeScript 会将其视为
Person。如果 中有更多属性me(例如 ),professionTypeScript 将不允许您访问它们。它未在 中定义Person。
Also, from this moment on, me is of type Person, and TypeScript will treat it as a
Person. If there are more properties in me—for example, a profession—TypeScript won’t allow you to access them. It’s not defined in Person.
如果向函数签名的返回值添加类型注释,则会告诉编译器在返回该值时检查类型是否匹配:
If you add a type annotation to a function signature’s return value, you tell the compiler to check if types match the moment you return that value:
functioncreatePerson():Person{return{name:"Stefan",age:39};}
functioncreatePerson():Person{return{name:"Stefan",age:39};}
如果返回的内容不匹配Person,TypeScript 将抛出错误。如果您想完全确保返回正确的类型,请执行此操作。如果您正在使用从各种来源构造大对象的函数,这尤其有用。
If you return something that doesn’t match Person, TypeScript will throw an error. Do this if you want to be completely sure that you return the correct type. This especially comes in handy if you are working with functions that construct big objects from various sources.
如果向函数签名的参数添加类型注释,则会告诉编译器在传递参数时检查类型是否匹配:
If you add a type annotation to a function signature’s parameters, you tell the compiler to check if types match the moment you pass along arguments:
functionprintPerson(person:Person){console.log(person.name,person.age);}printPerson(me);
functionprintPerson(person:Person){console.log(person.name,person.age);}printPerson(me);
在我看来,这是最重要且不可避免的类型注释。其他一切都可以推断出来:
In my opinion this is the most important and unavoidable type annotation. Everything else can be inferred:
typePerson={name:string;age:number;};// Inferred!// return type is { name: string, age: number }functioncreatePerson(){return{name:"Stefan",age:39};}// Inferred!// me: { name: string, age: number}constme=createPerson();// Annotated! You have to check if types are compatiblefunctionprintPerson(person:Person){console.log(person.name,person.age);}// All worksprintPerson(me);
typePerson={name:string;age:number;};// Inferred!// return type is { name: string, age: number }functioncreatePerson(){return{name:"Stefan",age:39};}// Inferred!// me: { name: string, age: number}constme=createPerson();// Annotated! You have to check if types are compatiblefunctionprintPerson(person:Person){console.log(person.name,person.age);}// All worksprintPerson(me);
你可以在需要注释的地方使用推断的对象类型,因为 TypeScript 具有结构类型系统。在结构类型系统中,编译器只会考虑类型的成员(属性),而不是实际名称。
You can use inferred object types where you expect an annotation because TypeScript has a structural type system. In a structural type system, the compiler will only take into account the members (properties) of a type, not the actual name.
如果要检查的类型的所有成员在值的类型中都可用,则类型兼容。我们还说类型的形状或结构必须匹配:
Types are compatible if all members of the type to check against are available in the type of the value. We also say that the shape or structure of a type has to match:
typePerson={name:string;age:number;};typeUser={name:string;age:number;id:number;};functionprintPerson(person:Person){console.log(person.name,person.age);}constuser:User={name:"Stefan",age:40,id:815,};printPerson(user);// works!
typePerson={name:string;age:number;};typeUser={name:string;age:number;id:number;};functionprintPerson(person:Person){console.log(person.name,person.age);}constuser:User={name:"Stefan",age:40,id:815,};printPerson(user);// works!
User比 具有更多属性Person,但 中的所有属性Person也都位于 中User,并且它们具有相同的类型。这就是为什么可以将User对象传递给printPerson,即使类型没有任何显式联系。
User has more properties than Person, but all properties that are in Person are also in User, and they have the same type. This is why it’s possible to pass User objects to printPerson, even though the types don’t have any explicit connection.
但是,如果你传递一个文字,TypeScript 会抱怨有不应该存在的多余属性:
However, if you pass a literal, TypeScript will complain that there are excess properties that should not be there:
printPerson({name:"Stefan",age:40,id:1000,// ^- Argument of type '{ name: string; age: number; id: number; }'// is not assignable to parameter of type 'Person'.// Object literal may only specify known properties,// and 'id' does not exist in type 'Person'.(2345)});
printPerson({name:"Stefan",age:40,id:1000,// ^- Argument of type '{ name: string; age: number; id: number; }'// is not assignable to parameter of type 'Person'.// Object literal may only specify known properties,// and 'id' does not exist in type 'Person'.(2345)});
这确保您没有想到此类型中会存在属性,然后想知道为什么更改它们没有效果。
This makes sure that you didn’t expect properties to be present in this type and then wonder why changing them has no effect.
使用结构类型系统,您可以使用具有推断类型的载体变量创建有趣的模式,并且可以在软件的不同部分重用相同的变量,而彼此之间没有类似的联系:
With a structural type system, you can create interesting patterns with carrier variables with the type inferred, and you can reuse the same variable in different parts of your software, with no similar connection to each other:
typePerson={name:string;age:number;};typeStudying={semester:number;};typeStudent={id:string;age:number;semester:number;};functioncreatePerson(){return{name:"Stefan",age:39,semester:25,id:"XPA"};}functionprintPerson(person:Person){console.log(person.name,person.age);}functionstudyForAnotherSemester(student:Studying){student.semester++;}functionisLongTimeStudent(student:Student){returnstudent.age-student.semester/2>30&&student.semester>20;}constme=createPerson();// All work!printPerson(me);studyForAnotherSemester(me);isLongTimeStudent(me);
typePerson={name:string;age:number;};typeStudying={semester:number;};typeStudent={id:string;age:number;semester:number;};functioncreatePerson(){return{name:"Stefan",age:39,semester:25,id:"XPA"};}functionprintPerson(person:Person){console.log(person.name,person.age);}functionstudyForAnotherSemester(student:Studying){student.semester++;}functionisLongTimeStudent(student:Student){returnstudent.age-student.semester/2>30&&student.semester>20;}constme=createPerson();// All work!printPerson(me);studyForAnotherSemester(me);isLongTimeStudent(me);
Student、Person和Studying有一些重叠,但彼此无关。createPerson返回与所有三种类型兼容的内容。如果注释过多,则需要创建比必要更多的类型和检查,而没有任何好处。
Student, Person, and Studying have some overlap but are unrelated to each other. createPerson returns something that is compatible with all three types. If you have annotated too much, you would need to create a lot more types and a lot more checks than necessary, without any benefit.
因此,请在需要检查类型的地方进行注释,至少对于函数 参数来说是这样。
So annotate wherever you want to have your types checked, at least for function arguments.
any当您想有效地停用打字功能时使用;unknown当您需要
小心谨慎时使用。
Use any if you effectively want to deactivate typing; use unknown when you need to
be cautious.
和any都是顶级类型,这意味着每个值都与或unknown兼容:anyunknown
Both any and unknown are top types, which means that every value is compatible with any or unknown:
constname:any="Stefan";constperson:any={name:"Stefan",age:40};constnotAvailable:any=undefined;
constname:any="Stefan";constperson:any={name:"Stefan",age:40};constnotAvailable:any=undefined;
由于any是每个值都兼容的类型,因此您可以不受限制地访问任何属性:
Since any is a type every value is compatible with, you can access any property without restriction:
constname:any="Stefan";// This is ok for TypeScript, but will crash in JavaScriptconsole.log(name.profession.experience[0].level);
constname:any="Stefan";// This is ok for TypeScript, but will crash in JavaScriptconsole.log(name.profession.experience[0].level);
any还与除 之外的每个子类型兼容never。这意味着你可以通过分配新类型来缩小可能的值集:
any is also compatible with every subtype, except never. This means you can narrow the set of possible values by assigning a new type:
constme:any="Stefan";// Good!constname:string=me;// Bad, but ok for the type system.constage:number=me;
constme:any="Stefan";// Good!constname:string=me;// Bad, but ok for the type system.constage:number=me;
any由于您实际上停用了类型检查,因此如此宽容可能会成为潜在错误和陷阱的持续来源。
Being so permissive, any can be a constant source of potential errors and pitfalls since you effectively deactivate type-checking.
虽然每个人似乎都同意你不应该any在代码库中使用,但在某些情况下any它确实很有用:
While everybody seems to agree that you shouldn’t use any in your codebases, there are some situations where any is really useful:
当您从 JavaScript 转到 TypeScript 时,您很可能已经拥有一个庞大的代码库,其中包含大量有关数据结构和对象如何工作的隐含信息。一次性把所有信息都说清楚可能很麻烦。any可以帮助您逐步迁移到更安全的代码库。
When you go from JavaScript to TypeScript, chances are that you already have a large codebase with a lot of implicit information on how your data structures and objects work. It might be a chore to get everything spelled out in one go. any can help you migrate to a safer codebase incrementally.
您可能有一个 JavaScript 依赖项,但仍然拒绝使用 TypeScript(或类似的东西)。或者更糟的是:没有最新的类型。Definitely Typed 是一个很好的资源,但它也是由志愿者维护的。它是 JavaScript 中存在但并非直接派生的东西的形式化。可能会有错误(即使在像 React 这样流行的类型定义中),或者它们可能不是最新的!
这是any可以帮到你的地方。如果你知道这个库是如何工作的,如果文档足够好,可以让你开始使用,并且如果你谨慎地使用它,那么any这可能是一种选择,而不是与类型作斗争。
You might have a JavaScript dependency that still refuses to use TypeScript (or something similar). Or even worse: there are no up-to-date types for it. Definitely Typed is a great resource, but it’s also maintained by volunteers. It’s a formalization of something that exists in JavaScript but is not directly derived from it. There might be errors (even in such popular type definitions like React’s), or they just might not be up to date!
This is where any can help you. When you know how the library works, if the documentation is good enough to get you going, and if you use it sparingly, any can be an option instead of fighting types.
TypeScript 的工作方式与 JavaScript 略有不同,需要做出很多权衡,以确保您不会遇到极端情况。这也意味着,如果您编写了某些可以在 JavaScript 中运行的内容,那么您会在 TypeScript 中遇到错误:
TypeScript works a bit differently from JavaScript and needs to make a lot of trade-offs to ensure that you don’t run into edge cases. This also means that if you write certain things that would work in JavaScript, you’d get errors in TypeScript:
typePerson={name:string;age:number;};functionprintPerson(person:Person){for(letkeyinperson){console.log(`${key}:${person[key]}`);// Element implicitly has an 'any' --^// type because expression of type 'string'// can't be used to index type 'Person'.// No index signature with a parameter of type 'string'// was found on type 'Person'.(7053)}}
typePerson={name:string;age:number;};functionprintPerson(person:Person){for(letkeyinperson){console.log(`${key}:${person[key]}`);// Element implicitly has an 'any' --^// type because expression of type 'string'// can't be used to index type 'Person'.// No index signature with a parameter of type 'string'// was found on type 'Person'.(7053)}}
请参阅9.1 节中的错误原因。在这种情况下,any可以帮助您暂时关闭类型检查,因为您知道自己在做什么。而且由于您可以从每种类型转到any,也可以返回到其他每种类型,因此在整个代码中,您都有一些明确的不安全代码块,您可以控制正在发生的事情:
Find out why this is an error in Recipe 9.1. In cases like this, any can help you to switch off type-checking for a moment because you know what you’re doing. And since you can go from every type to any, but also back to every other type, you have little, explicit unsafe blocks throughout your code where you are in charge of what’s happening:
functionprintPerson(person:any){for(letkeyinperson){console.log(`${key}:${person[key]}`);}}
functionprintPerson(person:any){for(letkeyinperson){console.log(`${key}:${person[key]}`);}}
一旦你知道这部分代码可以工作,你就可以开始添加正确的类型,解决 TypeScript 的限制,并输入断言:
Once you know this part of your code works, you can start adding the right types, work around TypeScript’s restrictions, and type assertions:
functionprintPerson(person:Person){for(letkeyinperson){console.log(`${key}:${person[keyaskeyofPerson]}`);}}
functionprintPerson(person:Person){for(letkeyinperson){console.log(`${key}:${person[keyaskeyofPerson]}`);}}
无论何时使用any,请确保noImplicitAny在tsconfig.json中激活该标志;默认情况下,它在模式下激活。当您没有通过推断或注释获得类型时,strictTypeScript 需要您明确注释。这有助于以后发现潜在的问题情况。any
Whenever you use any, make sure you activate the noImplicitAny flag in your tsconfig.json; it is activated by default in strict mode. TypeScript needs you to explicitly annotate any when you don’t have a type through inference or annotation. This helps find potentially problematic situations later on.
的替代方案any是unknown。它允许相同的值,但你可以用它做的事情非常不同。Whereany允许你做所有事情,unknownWhere 不允许你做任何事情。你所能做的就是传递值;当你想要调用一个函数或使类型更具体时,你首先需要进行类型检查:
An alternative to any is unknown. It allows for the same values, but the things you can do with it are very different. Where any allows you to do everything, unknown allows you to do nothing. All you can do is pass values around; the moment you want to call a function or make the type more specific, you first need to do type-checks:
constme:unknown="Stefan";constname:string=me;// ^- Type 'unknown' is not assignable to type 'string'.(2322)constage:number=me;// ^- Type 'unknown' is not assignable to type 'number'.(2322)
constme:unknown="Stefan";constname:string=me;// ^- Type 'unknown' is not assignable to type 'string'.(2322)constage:number=me;// ^- Type 'unknown' is not assignable to type 'number'.(2322)
类型检查和控制流分析可帮助您做更多unknown:
Type-checks and control flow analysis help you do more with unknown:
functiondoSomething(value:unknown){if(typeofvalue==="string"){// value: stringconsole.log("It's a string",value.toUpperCase());}elseif(typeofvalue==="number"){// value: numberconsole.log("it's a number",value*2);}}
functiondoSomething(value:unknown){if(typeofvalue==="string"){// value: stringconsole.log("It's a string",value.toUpperCase());}elseif(typeofvalue==="number"){// value: numberconsole.log("it's a number",value*2);}}
如果您的应用程序使用许多不同类型,unknown那么这非常适合确保您可以在整个代码中携带值,但不会因为的any许可而遇到任何安全问题。
If your apps work with a lot of different types, unknown is great for making sure that you can carry values throughout your code but don’t run into any safety problems because of any’s permissiveness.
用于object对象、函数和数组等复合类型。用于{}所有具有值的东西。
Use object for compound types like objects, functions, and arrays. Use {} for everything that has a value.
TypeScript 将其类型分为两个分支。第一个分支,原始类型,包括number、、、、boolean和一些子类型。stringsymbolbigint第二个分支,复合类型,包括对象的子类型的所有内容,最终由其他复合类型或原始类型组成。图 2-1提供了
概览。
TypeScript divides its types into two branches. The first branch, primitive types, includes number, boolean, string, symbol, bigint, and some subtypes. The second branch, compound types, includes everything that is a subtype of an object and is ultimately composed of other compound types or primitive types. Figure 2-1 provides
an overview.
在某些情况下,您希望将目标值设为复合类型,因为您想要修改某些属性,或者只是因为您只想确保不传递任何原始值。例如,Object.create创建一个新对象并将其原型作为第一个参数。这只能是复合类型;否则,您的运行时 JavaScript 代码将崩溃:
In some situations you want to target values that are compound types, either because you want to modify certain properties or because you just want to be safe that you don’t pass any primitive values. For example Object.create creates a new object and takes its prototype as the first argument. This can only be a compound type; otherwise, your runtime JavaScript code would crash:
Object.create(2);// Uncaught TypeError: Object prototype may only be an Object or null: 2// at Function.create (<anonymous>)
Object.create(2);// Uncaught TypeError: Object prototype may only be an Object or null: 2// at Function.create (<anonymous>)
在 TypeScript 中,三种类型似乎做同样的事情:空对象类型{}、大写 OObject接口和小写 Oobject类型。对于复合类型,您会使用哪一种?
In TypeScript, three types seem to do the same thing: The empty object type {}, the uppercase O Object interface, and the lowercase O object type. Which one do you use for compound types?
{}并Object允许大致相同的值,即除了null或之外的所有内容undefined(假设strict模式或被strictNullChecks激活):
{} and Object allow for roughly the same values, which are everything but null or undefined (given that strict mode or strictNullChecks is activated):
letobj:{};// Similar to Objectobj=32;obj="Hello";obj=true;obj=()=>{console.log("Hello")};obj=undefined;// Errorobj=null;// Errorobj={name:"Stefan",age:40};obj=[];obj=/.*/;
letobj:{};// Similar to Objectobj=32;obj="Hello";obj=true;obj=()=>{console.log("Hello")};obj=undefined;// Errorobj=null;// Errorobj={name:"Stefan",age:40};obj=[];obj=/.*/;
该Object接口与所有具有原型的值兼容Object,即每个原始类型和复合类型的每个值。
The Object interface is compatible with all values that have the Object prototype, which is every value from every primitive and compound type.
但是,Object是 TypeScript 中定义的接口,它对某些功能有一些要求。例如,方法toString是toString() => string任何非空值的一部分,是Object原型的一部分。如果你用不同的方法赋值tostring,TypeScript 会出错:
However, Object is a defined interface in TypeScript, and it has some requirements for certain functions. For example, the toString method, which is toString() => string and part of any non-nullish value, is part of the Object prototype. If you assign a value with a different tostring method, TypeScript will error:
letokObj:{}={toString(){returnfalse;}};// OKletobj:Object={toString(){returnfalse;}// ^- Type 'boolean' is not assignable to type 'string'.ts(2322)}
letokObj:{}={toString(){returnfalse;}};// OKletobj:Object={toString(){returnfalse;}// ^- Type 'boolean' is not assignable to type 'string'.ts(2322)}
Object可能会由于这种行为而引起一些混乱,因此在大多数情况下,您可以使用{}。
Object can cause some confusion due to this behavior, so in most cases, you’re good with {}.
TypeScript 也有小写 object类型。这更符合你的要求,因为它允许任何复合类型,但不允许原始类型:
TypeScript also has a lowercase object type. This is more the type you’re looking for, as it allows for any compound type but no primitive types:
letobj:object;obj=32;// Errorobj="Hello";// Errorobj=true;// Errorobj=()=>{console.log("Hello")};obj=undefined;// Errorobj=null;// Errorobj={name:"Stefan",age:40};obj=[];obj=/.*/;
letobj:object;obj=32;// Errorobj="Hello";// Errorobj=true;// Errorobj=()=>{console.log("Hello")};obj=undefined;// Errorobj=null;// Errorobj={name:"Stefan",age:40};obj=[];obj=/.*/;
如果您想要一种不包含函数、正则表达式、数组等的类型,请参阅第 5 章,我们将在其中自行创建一个。
If you want a type that excludes functions, regexes, arrays, and the like, see Chapter 5, where we create one on our own.
用元组类型进行注释。
Annotate with tuple types.
与对象一样,JavaScript 数组是组织复杂对象中数据的一种流行方式。Person我们在其他方案中编写了一个典型的对象,而现在您可以逐个元素地存储条目:
Like objects, JavaScript arrays are a popular way to organize data in a complex object. Instead of writing a typical Person object as we did in other recipes, you can store entries element by element:
constperson=["Stefan",40];// name and age
constperson=["Stefan",40];// name and age
与对象相比,使用数组的好处是数组元素没有属性名称。当你使用解构将每个元素分配给变量时,分配自定义名称变得非常容易:
The benefit of using arrays over objects is that array elements don’t have property names. When you assign each element to variables using destructuring, it becomes really easy to assign custom names:
// objects.js// Using objectsconstperson={name:"Stefan",age:40,};const{name,age}=person;console.log(name);// Stefanconsole.log(age);// 40const{anotherName=name,anotherAge=age}=person;console.log(anotherName);// Stefanconsole.log(anotherAge);// 40// arrays.js// Using arraysconstperson=["Stefan",40];// name and ageconst[name,age]=person;console.log(name);// Stefanconsole.log(age);// 40const[anotherName,anotherAge]=person;console.log(anotherName);// Stefanconsole.log(anotherAge);// 40
// objects.js// Using objectsconstperson={name:"Stefan",age:40,};const{name,age}=person;console.log(name);// Stefanconsole.log(age);// 40const{anotherName=name,anotherAge=age}=person;console.log(anotherName);// Stefanconsole.log(anotherAge);// 40// arrays.js// Using arraysconstperson=["Stefan",40];// name and ageconst[name,age]=person;console.log(name);// Stefanconsole.log(age);// 40const[anotherName,anotherAge]=person;console.log(anotherName);// Stefanconsole.log(anotherAge);// 40
对于需要不断分配新名称的 API,使用数组非常舒服,如第 10 章所述。
For APIs where you need to assign new names constantly, using arrays is very comfortable, as explained in Chapter 10.
但是,当使用 TypeScript 并依赖类型推断时,此模式可能会导致一些问题。默认情况下,TypeScript 从赋值中推断数组类型。数组是开放式集合,每个位置都有相同的元素:
When using TypeScript and relying on type inference, however, this pattern can cause some issues. By default, TypeScript infers the array type from an assignment. Arrays are open-ended collections with the same element in each position:
constperson=["Stefan",40];// person: (string | number)[]
constperson=["Stefan",40];// person: (string | number)[]
因此 TypeScript 认为这person是一个数组,其中每个元素可以是字符串或数字,并且它允许在原始两个元素之后添加大量元素。这意味着当你解构时,每个元素也是string或类型number:
So TypeScript thinks that person is an array, where each element can be either a string or a number, and it allows for plenty of elements after the original two. This means when you’re destructuring, each element is also of type string or number:
const[name,age]=person;// name: string | number// age: string | number
const[name,age]=person;// name: string | number// age: string | number
这使得 JavaScript 中舒适的模式在 Typescript 中变得非常麻烦。您需要进行控制流检查以将类型缩小到实际类型,但从分配中应该可以清楚地看出,这是不必要的。
That makes a comfortable pattern in JavaScript really cumbersome in Typescript. You would need to do control flow checks to narrow the type to the actual one, where it should be clear from the assignment that this is not necessary.
每当您需要在 JavaScript 中做额外的工作才能满足 TypeScript 时,通常都有更好的方法。在这种情况下,您可以使用元组类型来更具体地说明如何解释数组。
Whenever you think you need to do extra work in JavaScript just to satisfy TypeScript, there’s usually a better way. In that case, you can use tuple types to be more specific about how your array should be interpreted.
元组类型是数组类型的兄弟,但语义不同。数组的大小可能无限大,并且每个元素的类型都相同(无论多大),而元组类型的大小是固定的,并且每个元素的类型都不同。
Tuple types are a sibling of array types that work on a different semantic. While arrays can be potentially endless in size and each element is of the same type (no matter how broad), tuple types have a fixed size, and each element has a distinct type.
要获取元组类型,只需明确注释:
All you need to do to get tuple types is to explicitly annotate:
constperson:[string,number]=["Stefan",40];const[name,age]=person;// name: string// age: number
constperson:[string,number]=["Stefan",40];const[name,age]=person;// name: string// age: number
太棒了!元组类型具有固定长度;这意味着长度也编码在类型中。因此,超出范围的赋值是不可能的;TypeScript 会抛出错误:
Fantastic! Tuple types have a fixed length; this means the length is also encoded in the type. So assignments that go out of bounds are not possible; TypeScript will throw an error:
person[1]=41;// OK!person[2]=false;// Error//^- Type 'false' is not assignable to type 'undefined'.(2322)
person[1]=41;// OK!person[2]=false;// Error//^- Type 'false' is not assignable to type 'undefined'.(2322)
TypeScript 还允许你为元组类型添加标签。这只是编辑器和编译器反馈的元信息,但它可以让你更清楚地了解每个元素的含义:
TypeScript also allows you to add labels to tuple types. This is just metainformation for editors and compiler feedback, but it allows you to be clearer about what to expect from each element:
typePerson=[name:string,age:number];
typePerson=[name:string,age:number];
这将帮助您和您的同事了解预期的内容,就像对象类型一样。
This will help you and your colleagues to understand what to expect, just like with object types.
元组类型也可用于注释函数参数。此函数:
Tuple types can also be used to annotate function arguments. This function:
functionhello(name:string,msg:string):void{// ...}
functionhello(name:string,msg:string):void{// ...}
也可以用元组类型来写:
can also be written with tuple types:
functionhello(...args:[name:string,msg:string]):{// ...}
functionhello(...args:[name:string,msg:string]):{// ...}
并且你可以非常灵活地定义它:
And you can be very flexible in defining it:
functionh(a:string,b:string,c:string):void{//...}// equal tofunctionh(a:string,b:string,...r:[string]):void{//...}// equal tofunctionh(a:string,...r:[string,string]):void{//...}// equal tofunctionh(...r:[string,string,string]):void{//...}
functionh(a:string,b:string,c:string):void{//...}// equal tofunctionh(a:string,b:string,...r:[string]):void{//...}// equal tofunctionh(a:string,...r:[string,string]):void{//...}// equal tofunctionh(...r:[string,string,string]):void{//...}
这些也被称为剩余元素,是 JavaScript 中的元素,允许您定义具有几乎无限参数列表的函数;当它是最后一个元素时,剩余元素会吸收所有多余的参数。当您需要在代码中收集参数时,可以在将它们应用于函数之前使用元组:
These are also known as rest elements, something that we have in JavaScript that allow you to define functions with an almost limitless argument list; when it is the last element, the rest element sucks all excess arguments in. When you need to collect arguments in your code, you can use a tuple before you apply them to your function:
constperson:[string,number]=["Stefan",40];functionhello(...args:[name:string,msg:string]):{// ...}hello(...person);
constperson:[string,number]=["Stefan",40];functionhello(...args:[name:string,msg:string]):{// ...}hello(...person);
元组类型在许多场景中都很有用。有关元组类型的更多信息,请参阅第 7 章和第10章。
Tuple types are useful for many scenarios. For more information about tuple types, see Chapters 7 and 10.
对项目边界内的类型使用类型别名,并对供其他人使用的契约使用接口。
Use type aliases for types within your project’s boundary, and use interfaces for contracts that are meant to be consumed by others.
多年来,定义对象类型的两种方法一直是许多博客文章的主题。随着时间的推移,它们都过时了。截至撰写本文时,类型别名和接口之间几乎没有区别。所有不同之处都已逐渐统一。
Both approaches to defining object types have been the subject of many blog articles over the years. And all of them became outdated over time. As of this writing there is little difference between type aliases and interfaces. And everything that was different has been gradually aligned.
从语法上来说,接口和类型别名之间的区别是细微的:
Syntactically, the difference between interfaces and type aliases is nuanced:
typePersonAsType={name:string;age:number;address:string[];greet():string;};interfacePersonAsInterface{name:string;age:number;address:string[];greet():string;}
typePersonAsType={name:string;age:number;address:string[];greet():string;};interfacePersonAsInterface{name:string;age:number;address:string[];greet():string;}
您可以在相同的场景中对相同的事物使用接口和类型别名:
You can use interfaces and type aliases for the same things, in the same scenarios:
在类的 implements 声明中
In an implements declaration for classes
作为对象字面量的类型注解
As a type annotation for object literals
对于递归类型结构
For recursive type structures
但是,有一个重要的区别可能会导致您通常不想处理的副作用:接口允许声明合并,但类型别名不允许。声明合并允许在声明接口后向其添加属性:
However, there is one important difference that can cause side effects you usually don’t want to deal with: interfaces allow for declaration merging, but type aliases don’t. Declaration merging allows for adding properties to an interface even after it has been declared:
interfacePerson{name:string;}interfacePerson{age:number;}// Person is now { name: string; age: number; }
interfacePerson{name:string;}interfacePerson{age:number;}// Person is now { name: string; age: number; }
TypeScript 经常在lib.d.ts文件中使用此技术,这样就可以根据 ECMAScript 版本添加新的 JavaScript API 增量。如果你想扩展,这是一个很棒的功能,Window但在其他情况下可能会适得其反,例如:
TypeScript often uses this technique in lib.d.ts files, making it possible to just add deltas of new JavaScript APIs based on ECMAScript versions. This is a great feature if you want to extend, for example, Window, but it can backfire in other scenarios, for example:
// Some data we collect in a web forminterfaceFormData{name:string;age:number;address:string[];}// A function that sends this data to a backendfunctionsend(data:FormData){console.log(data.entries())// this compiles!// but crashes horrendously in runtime}
// Some data we collect in a web forminterfaceFormData{name:string;age:number;address:string[];}// A function that sends this data to a backendfunctionsend(data:FormData){console.log(data.entries())// this compiles!// but crashes horrendously in runtime}
那么,该entries()方法从何而来?它是一个 DOM API!FormData是浏览器 API 提供的接口之一,并且有很多这样的接口。它们是全局可用的,没有什么可以阻止您扩展这些接口。如果您这样做,您不会收到任何通知。
So, where does the entries() method come from? It’s a DOM API! FormData is one of the interfaces provided by browser APIs, and there are a lot of them. They are globally available, and nothing keeps you from extending those interfaces. And you get no notification if you do.
当然,您可以争论正确的命名,但对于您全局提供的所有接口,问题仍然存在,也许是由于某些依赖关系,您甚至不知道它们将接口添加到全局空间。
You can of course argue about proper naming, but the problem persists for all interfaces that you make available globally, maybe from some dependency where you aren’t even aware they add an interface to the global space.
将此接口更改为类型别名会立即让您意识到这个问题:
Changing this interface to a type alias immediately makes you aware of this problem:
typeFormData={// ^-- Duplicate identifier 'FormData'.(2300)name:string;age:number;address:string[];};
typeFormData={// ^-- Duplicate identifier 'FormData'.(2300)name:string;age:number;address:string[];};
如果您要创建一个库,供项目中的其他部分使用,甚至可能供其他团队编写的其他项目使用,则声明合并是一项很棒的功能。它允许您定义一个描述应用程序的接口,但允许您的用户根据实际情况进行调整。想象一下插件系统,加载新模块可以增强功能:声明合并是一项您不想错过的功能。
Declaration merging is a fantastic feature if you are creating a library that is consumed by other parts in your project, maybe even other projects written entirely by other teams. It allows you to define an interface that describes your application but allows your users to adapt it to reality. Think of a plug-in system, where loading new modules enhances functionality: declaration merging is a feature that you do not want to miss.
然而,在模块的边界内,使用类型别名可以防止您意外重用或扩展已声明的类型。当您不希望其他人使用它们时,请使用类型别名。
Within your module’s boundaries, however, using type aliases prevents you from accidentally reusing or extending already declared types. Use type aliases when you don’t expect others to consume them.
在接口上使用类型别名引发了一些讨论,因为接口在评估中被认为比类型别名具有更高的性能,甚至在官方TypeScript wiki上提出了性能建议。此建议应谨慎对待。
Using type aliases over interfaces has sparked some discussion, as interfaces have been considered much more performant in their evaluation than type aliases, even resulting in a performance recommendation on the official TypeScript wiki. This recommendation should be taken with a grain of salt.
在创建时,简单类型别名的执行速度可能比接口更快,因为接口永远不会关闭,并且可能与其他声明合并。但接口在其他地方的执行速度可能更快,因为它们事先就被知道是对象类型。TypeScript 团队的 Ryan Canavaugh 预计,如果声明的接口或类型别名数量非常多,性能差异将是可衡量的:根据这条推文,大约有五千个。
On creation, simple type aliases may perform faster than interfaces because interfaces are never closed and might be merged with other declarations. But interfaces may perform faster in other places because they’re known ahead of time to be object types. Ryan Canavaugh from the TypeScript team expects performance differences to be measurable with an extraordinary number of interfaces or type aliases to be declared: around five thousand according to this tweet.
如果您的 TypeScript 代码库性能不佳,那并不是因为您声明了太多类型别名而不是接口,反之亦然。
If your TypeScript code base doesn’t perform well, it’s not because you declared too many type aliases instead of interfaces, or vice versa.
使用函数重载。
Use function overloads.
JavaScript 在函数参数方面非常灵活。基本上可以传递任意长度的任何参数。只要函数体正确处理输入,就没问题。这允许非常符合人体工程学的 API,但输入起来也非常困难。
JavaScript is very flexible when it comes to function arguments. You can pass basically any parameters, of any length. As long as the function body treats the input correctly, you’re good. This allows for very ergonomic APIs, but it’s also very tough to type.
想象一下概念上的任务运行器。使用task函数,您可以按名称定义新任务,并传递回调或要执行的其他任务列表。或者两者兼而有之——在回调运行之前需要执行的任务列表:
Think of a conceptual task runner. With a task function you define new tasks by name and either pass a callback or pass a list of other tasks to be executed. Or both—a list of tasks that needs to be executed before the callback runs:
task("default",["scripts","styles"]);task("scripts",["lint"],()=>{// ...});task("styles",()=>{// ...});
task("default",["scripts","styles"]);task("scripts",["lint"],()=>{// ...});task("styles",()=>{// ...});
如果您认为“这看起来很像六年前的 Gulp”,那么您是对的。其灵活的 API 让您不会犯太多错误,这也是 Gulp 如此 受欢迎的原因之一。
If you’re thinking, “this looks a lot like Gulp six years ago,” you’re right. Its flexible API where you couldn’t do much wrong was also one of the reasons Gulp was so popular.
像这样输入函数可能是一个噩梦。可选参数,同一位置的不同类型——即使使用联合类型,这也很难做到:1
Typing functions like this can be a nightmare. Optional arguments, different types at the same position—this is tough to do even if you use union types:1
typeCallbackFn=()=>void;functiontask(name:string,param2:string[]|CallbackFn,param3?:CallbackFn):void{//...}
typeCallbackFn=()=>void;functiontask(name:string,param2:string[]|CallbackFn,param3?:CallbackFn):void{//...}
这捕获了前面示例中的所有变化,但它也是错误的,因为它允许毫无意义的组合:
This catches all variations from the preceding example, but it’s also wrong, as it allows for combinations that don’t make any sense:
task("what",()=>{console.log("Two callbacks?");},()=>{console.log("That's not supported, but the types say yes!");});
task("what",()=>{console.log("Two callbacks?");},()=>{console.log("That's not supported, but the types say yes!");});
值得庆幸的是,TypeScript 有一种方法可以解决此类问题:函数重载。它的名字暗示了其他编程语言中的类似概念:定义相同但行为不同。与其他编程语言相比,TypeScript 最大的不同在于函数重载仅在类型系统级别起作用,对实际实现没有影响。
Thankfully, TypeScript has a way to solve problems like this: function overloads. Its name hints at similar concepts from other programming languages: the same defintion but with different behavior. The biggest difference in TypeScript, as opposed to other programming languages, is that function overloads work only on a type system level and have no effect on the actual implementation.
这个想法是,你将每个可能的情况定义为其自己的函数签名。最后一个函数签名是实际的实现:
The idea is that you define every possible scenario as its own function signature. The last function signature is the actual implementation:
// Types for the type systemfunctiontask(name:string,dependencies:string[]):void;functiontask(name:string,callback:CallbackFn):voidfunctiontask(name:string,dependencies:string[],callback:CallbackFn):void// The actual implementationfunctiontask(name:string,param2:string[]|CallbackFn,param3?:CallbackFn):void{//...}
// Types for the type systemfunctiontask(name:string,dependencies:string[]):void;functiontask(name:string,callback:CallbackFn):voidfunctiontask(name:string,dependencies:string[],callback:CallbackFn):void// The actual implementationfunctiontask(name:string,param2:string[]|CallbackFn,param3?:CallbackFn):void{//...}
这里有几件事需要注意。
A couple of things are important to note here.
首先,TypeScript 仅选择实际实现之前的声明作为可能的类型。如果实际实现签名也相关,则复制它。
First, TypeScript only picks up the declarations before the actual implementation as possible types. If the actual implementation signature is also relevant, duplicate it.
此外,实际的实现函数签名不能是任何东西。TypeScript 检查重载是否可以使用实现签名来实现。
Also, the actual implementation function signature can’t be anything. TypeScript checks if the overloads can be implemented with the implementation signature.
如果您有不同的返回类型,则您有责任确保输入和输出匹配:
If you have different return types, it is your responsibility to make sure that inputs and outputs match:
functionfn(input:number):numberfunctionfn(input:string):stringfunctionfn(input:number|string):number|string{if(typeofinput==="number"){return"this also works";}else{return1337;}}consttypeSaysNumberButItsAString=fn(12);consttypeSaysStringButItsANumber=fn("Hello world");
functionfn(input:number):numberfunctionfn(input:string):stringfunctionfn(input:number|string):number|string{if(typeofinput==="number"){return"this also works";}else{return1337;}}consttypeSaysNumberButItsAString=fn(12);consttypeSaysStringButItsANumber=fn("Hello world");
实现签名通常适用于非常广泛的类型,这意味着您必须进行大量检查,而这些检查在 JavaScript 中无论如何都需要进行。这很好,因为它促使您格外小心。
The implementation signature usually works with a very broad type, which means you have to do a lot of checks that you would need to do in JavaScript anyway. This is good as it urges you to be extra careful.
如果您需要重载函数作为其自己的类型,以便在注释中使用它们并分配多个实现,您可以随时创建类型别名:
If you need overloaded functions as their own type, to use them in annotations and assign multiple implementations, you can always create a type alias:
typeTaskFn={(name:string,dependencies:string[]):void;(name:string,callback:CallbackFn):void;(name:string,dependencies:string[],callback:CallbackFn):void;}
typeTaskFn={(name:string,dependencies:string[]):void;(name:string,callback:CallbackFn):void;(name:string,dependencies:string[],callback:CallbackFn):void;}
As you can see, you only need the type system overloads, not the actual implementation definition.
this在函数签名的开头定义参数类型。
Define a this parameter type at the beginning of a function signature.
对于有抱负的 JavaScript 开发人员来说,一个令人困惑的来源是对象指针不断变化的性质this:
One source of confusion for aspiring JavaScript developers is the ever-changing nature of the this object pointer:
有时候,当我写 JavaScript 的时候,我想大喊:“这太荒谬了!”但是我从来不知道this指的是什么。
未知的 JavaScript 开发人员
Sometimes when writing JavaScript, I want to shout, “This is ridiculous!” But then I never know what this refers to.
Unknown JavaScript developer
上述说法是正确的,特别是如果你的背景是基于类的面向对象编程语言,其中this总是指类的实例。JavaScriptthis中的完全不同,但并不一定更难理解。更重要的是,TypeScript 可以极大地帮助在使用中获得更多封闭性this。
The preceding statement is true especially if your background is a class-based object-oriented programming language, where this always refers to an instance of a class. this in JavaScript is entirely different but not necessarily harder to understand. What’s more, TypeScript can greatly help get more closure about this in usage.
this存在于函数范围内,并指向绑定到该函数的对象或值。在常规对象中,this非常简单:
this lives within the scope of a function, and that points to an object or value bound to that function. In regular objects, this is pretty straightforward:
constauthor={name:"Stefan",// function shorthandhi(){console.log(this.name);},};author.hi();// prints 'Stefan'
constauthor={name:"Stefan",// function shorthandhi(){console.log(this.name);},};author.hi();// prints 'Stefan'
但函数在 JavaScript 中是值,它们可以绑定到不同的上下文,从而有效地改变值this:
But functions are values in JavaScript, and they can be bound to a different context, effectively changing the value of this:
constauthor={name:"Stefan",};functionhi(){console.log(this.name);}constpet={name:"Finni",kind:"Cat",};hi.apply(pet);// prints "Finni"hi.call(author);// prints "Stefan"constboundHi=hi.bind(author);boundHi();// prints "Stefan"
constauthor={name:"Stefan",};functionhi(){console.log(this.name);}constpet={name:"Finni",kind:"Cat",};hi.apply(pet);// prints "Finni"hi.call(author);// prints "Stefan"constboundHi=hi.bind(author);boundHi();// prints "Stefan"
this如果使用箭头函数而不是常规函数,那么语义的再次改变就无济于事了:
It doesn’t help that the semantics of this change again if you use arrow functions instead of regular functions:
classPerson{constructor(name){this.name=name;}hi(){console.log(this.name);}hi_timeout(){setTimeout(function(){console.log(this.name);},0);}hi_timeout_arrow(){setTimeout(()=>{console.log(this.name);},0);}}constperson=newPerson("Stefan")person.hi();// prints "Stefan"person.hi_timeout();// prints "undefined"person.hi_timeout_arrow();// prints "Stefan"
classPerson{constructor(name){this.name=name;}hi(){console.log(this.name);}hi_timeout(){setTimeout(function(){console.log(this.name);},0);}hi_timeout_arrow(){setTimeout(()=>{console.log(this.name);},0);}}constperson=newPerson("Stefan")person.hi();// prints "Stefan"person.hi_timeout();// prints "undefined"person.hi_timeout_arrow();// prints "Stefan"
使用 TypeScript,我们可以通过参数类型获得更多关于它this是什么以及更重要的是它应该是什么的信息。this
With TypeScript, we can get more information on what this is and, more importantly, what it’s supposed to be through this parameter types.
请看以下示例。我们通过 DOM API 访问按钮元素并为其绑定事件侦听器。在回调函数中,this是 类型HTMLButtonElement,这意味着您可以访问以下属性classList:
Take a look at the following example. We access a button element via DOM APIs and bind an event listener to it. Within the callback function, this is of type HTMLButtonElement, which means you can access properties like classList:
constbutton=document.querySelector("button");button?.addEventListener("click",function(){this.classList.toggle("clicked");});
constbutton=document.querySelector("button");button?.addEventListener("click",function(){this.classList.toggle("clicked");});
的信息this由函数提供addEventListener。如果你在重构步骤中提取函数,则可以保留功能,但 TypeScript 会出错,因为它会丢失上下文this:
The information on this is provided by the addEventListener function. If you extract your function in a refactoring step, you retain the functionality, but TypeScript will error, as it loses context for this:
constbutton=document.querySelector("button");button.addEventListener("click",handleToggle);functionhandleToggle(){this.classList.toggle("clicked");// ^- 'this' implicitly has type 'any'// because it does not have a type annotation}
constbutton=document.querySelector("button");button.addEventListener("click",handleToggle);functionhandleToggle(){this.classList.toggle("clicked");// ^- 'this' implicitly has type 'any'// because it does not have a type annotation}
诀窍是告诉 TypeScript 这this应该是一个特定的类型。你可以在函数签名的第一个位置添加一个名为的参数来实现这一点this:
The trick is to tell TypeScript that this is supposed to be a specific type. You can do this by adding a parameter at the very first position in your function signature named this:
constbutton=document.querySelector("button");button?.addEventListener("click",handleToggle);functionhandleToggle(this:HTMLButtonElement){this.classList.toggle("clicked");}
constbutton=document.querySelector("button");button?.addEventListener("click",handleToggle);functionhandleToggle(this:HTMLButtonElement){this.classList.toggle("clicked");}
编译后,此参数将被删除。 TypeScript 现在拥有确保需要this为类型所需的所有信息,这也意味着一旦在不同的上下文中HTMLButtonElement使用就会出现错误:handleToggle
This argument gets removed once compiled. TypeScript now has all the information it needs to make sure this needs to be of type HTMLButtonElement, which also means that you get errors once you use handleToggle in a different context:
handleToggle();// ^- The 'this' context of type 'void' is not// assignable to method's 'this' of type 'HTMLButtonElement'.
handleToggle();// ^- The 'this' context of type 'void' is not// assignable to method's 'this' of type 'HTMLButtonElement'.
如果你定义为的超类型,那么你可以让它变得handleToggle更加有用:thisHTMLElementHTMLButtonElement
You can make handleToggle even more useful if you define this to be HTMLElement, a supertype of HTMLButtonElement:
constbutton=document.querySelector("button");button?.addEventListener("click",handleToggle);constinput=document.querySelector("input");input?.addEventListener("click",handleToggle);functionhandleToggle(this:HTMLElement){this.classList.toggle("clicked");}
constbutton=document.querySelector("button");button?.addEventListener("click",handleToggle);constinput=document.querySelector("input");input?.addEventListener("click",handleToggle);functionhandleToggle(this:HTMLElement){this.classList.toggle("clicked");}
使用this参数类型时,您可能需要使用两种可以this从函数类型中提取或删除参数的辅助类型:
When working with this parameter types, you might want to use two helper types that can either extract or remove this parameters from your function type:
functionhandleToggle(this:HTMLElement){this.classList.toggle("clicked");}typeToggleFn=typeofhandleToggle;// (this: HTMLElement) => voidtypeWithoutThis=OmitThisParameter<ToggleFn>// () = > voidtypeToggleFnThis=ThisParameterType<ToggleFn>// HTMLElement
functionhandleToggle(this:HTMLElement){this.classList.toggle("clicked");}typeToggleFn=typeofhandleToggle;// (this: HTMLElement) => voidtypeWithoutThis=OmitThisParameter<ToggleFn>// () = > voidtypeToggleFnThis=ThisParameterType<ToggleFn>// HTMLElement
类和对象中还有更多辅助类型this。更多内容请参见4.8 节和11.8 节。
There are more helper types for this in classes and objects. See more in Recipes 4.8 and 11.8.
为您希望唯一且不可迭代的对象属性创建符号。它们非常适合存储和访问敏感信息。
Create symbols for object properties you want to be unique and not iterable. They’re great for storing and accessing sensitive information.
symbol是 JavaScript 和 TypeScript 中的原始数据类型,除其他外,还可用于对象属性。与number和相比string,符号具有一些独特的功能。
symbol is a primitive data type in JavaScript and TypeScript, which, among other things, can be used for object properties. Compared to number and string, symbols have some unique features.
Symbols can be created using the Symbol() factory function:
constTITLE=Symbol('title')
constTITLE=Symbol('title')
Symbol没有构造函数。参数是可选的描述。通过调用工厂函数,TITLE分配这个新创建的符号的唯一值。这个符号现在是唯一的,可以与所有其他符号区分开来,并且不会与任何其他具有相同描述的符号冲突:
Symbol has no constructor function. The parameter is an optional description. By calling the factory function, TITLE is assigned the unique value of this freshly created symbol. This symbol is now unique and distinguishable from all other symbols, and it doesn’t clash with any other symbols that have the same description:
constACADEMIC_TITLE=Symbol('title')constARTICLE_TITLE=Symbol('title')if(ACADEMIC_TITLE===ARTICLE_TITLE){// This is never true}
constACADEMIC_TITLE=Symbol('title')constARTICLE_TITLE=Symbol('title')if(ACADEMIC_TITLE===ARTICLE_TITLE){// This is never true}
该描述可帮助您在开发期间获取有关符号的信息:
The description helps you to get info on the symbol during development time:
console.log(ACADEMIC_TITLE.description)// titleconsole.log(ACADEMIC_TITLE.toString())// Symbol(title)
console.log(ACADEMIC_TITLE.description)// titleconsole.log(ACADEMIC_TITLE.toString())// Symbol(title)
如果您想要具有唯一且排他性的可比较值,则符号非常有用。对于运行时开关或模式比较:
Symbols are great if you want to have comparable values that are exclusive and unique. For runtime switches or mode comparisons:
// A really bad logging frameworkconstLEVEL_INFO=Symbol('INFO')constLEVEL_DEBUG=Symbol('DEBUG')constLEVEL_WARN=Symbol('WARN')constLEVEL_ERROR=Symbol('ERROR')functionlog(msg,level){switch(level){caseLEVEL_WARN:console.warn(msg);breakcaseLEVEL_ERROR:console.error(msg);break;caseLEVEL_DEBUG:console.log(msg);debugger;break;caseLEVEL_INFO:console.log(msg);}}
// A really bad logging frameworkconstLEVEL_INFO=Symbol('INFO')constLEVEL_DEBUG=Symbol('DEBUG')constLEVEL_WARN=Symbol('WARN')constLEVEL_ERROR=Symbol('ERROR')functionlog(msg,level){switch(level){caseLEVEL_WARN:console.warn(msg);breakcaseLEVEL_ERROR:console.error(msg);break;caseLEVEL_DEBUG:console.log(msg);debugger;break;caseLEVEL_INFO:console.log(msg);}}
符号也可以用作属性键,但不可迭代,这对于 序列化非常有用:
Symbols also work as property keys but are not iterable, which is great for serialization:
const=Symbol('print')constuser={name:'Stefan',age:40,[]:function(){console.log(`${this.name}is${this.age}years old`)}}JSON.stringify(user)// { name: 'Stefan', age: 40 }user[]()// Stefan is 40 years old
const=Symbol('print')constuser={name:'Stefan',age:40,[]:function(){console.log(`${this.name}is${this.age}years old`)}}JSON.stringify(user)// { name: 'Stefan', age: 40 }user[]()// Stefan is 40 years old
全局符号注册表允许您访问整个应用程序中的令牌:
A global symbols registry allows you to access tokens across your whole application:
Symbol.for('print')// creates a global symbolconstuser={name:'Stefan',age:37,// uses the global symbol[Symbol.for('print')]:function(){console.log(`${this.name}is${this.age}years old`)}}
Symbol.for('print')// creates a global symbolconstuser={name:'Stefan',age:37,// uses the global symbol[Symbol.for('print')]:function(){console.log(`${this.name}is${this.age}years old`)}}
第一次调用Symbol.for创建一个符号,第二次调用使用相同的符号。如果将符号值存储在变量中并想知道键,则可以使用Symbol.keyFor():
The first call to Symbol.for creates a symbol, and the second call uses the same symbol. If you store the symbol value in a variable and want to know the key, you can use Symbol.keyFor():
constusedSymbolKeys=[]functionextendObject(obj,symbol,value){//Oh, what symbol is this?constkey=Symbol.keyFor(symbol)//Alright, let's better store thisif(!usedSymbolKeys.includes(key)){usedSymbolKeys.push(key)}obj[symbol]=value}// now it's time to retreive them allfunctionprintAllValues(obj){usedSymbolKeys.forEach(key=>{console.log(obj[Symbol.for(key)])})}
constusedSymbolKeys=[]functionextendObject(obj,symbol,value){//Oh, what symbol is this?constkey=Symbol.keyFor(symbol)//Alright, let's better store thisif(!usedSymbolKeys.includes(key)){usedSymbolKeys.push(key)}obj[symbol]=value}// now it's time to retreive them allfunctionprintAllValues(obj){usedSymbolKeys.forEach(key=>{console.log(obj[Symbol.for(key)])})}
好棒啊!
Nifty!
TypeScript 完全支持符号,它们是类型系统中的首要成员。symbol本身是所有可能符号的数据类型注释。请参阅extendObject前面代码块中的函数。为了允许所有符号扩展我们的对象,我们可以使用类型symbol:
TypeScript has full support for symbols, and they are prime citizens in the type system. symbol itself is a data type annotation for all possible symbols. See the extendObject function in the preceding code block. To allow for all symbols to extend our object, we can use the symbol type:
constsym=Symbol('foo')functionextendObject(obj:any,sym:symbol,value:any){obj[sym]=value}extendObject({},sym,42)// Works with all symbols
constsym=Symbol('foo')functionextendObject(obj:any,sym:symbol,value:any){obj[sym]=value}extendObject({},sym,42)// Works with all symbols
还有子类型unique symbol。Aunique symbol与声明紧密相关,只允许在const声明中使用,并且引用此精确符号而不引用其他任何符号。
There’s also the subtype unique symbol. A unique symbol is closely tied to the declaration, allowed only in const declarations, and referencing this exact symbol and nothing else.
您可以将 TypeScript 中的名义类型视为 JavaScript 中的非常名义的值。
You can think of a nominal type in TypeScript for a very nominal value in JavaScript.
要获得 的类型unique symbol,您需要使用typeof运算符:
To get to the type of unique symbol, you need to use the typeof operator:
constPROD:uniquesymbol=Symbol('Production mode')constDEV:uniquesymbol=Symbol('Development mode')functionshowWarning(msg:string,mode:typeofDEV|typeofPROD){// ...}
constPROD:uniquesymbol=Symbol('Production mode')constDEV:uniquesymbol=Symbol('Development mode')functionshowWarning(msg:string,mode:typeofDEV|typeofPROD){// ...}
在撰写本文时,唯一可能的名义类型是 TypeScript 的结构 类型系统。
At the time of writing, the only possible nominal type is TypeScript’s structural type system.
符号位于 TypeScript 和 JavaScript 中名义类型和不透明类型的交汇处。它们是我们在运行时最接近名义类型检查的东西。
Symbols stand at the intersection between nominal and opaque types in TypeScript and JavaScript. They are the closest things we get to nominal type-checks at runtime.
了解类型和值命名空间,以及哪些名称对什么有贡献。
Learn about type and value namespaces, and which names contribute to what.
TypeScript 是 JavaScript 的超集,这意味着它为已经存在且已定义的语言添加了更多内容。随着时间的推移,你会学会辨别哪些部分是 JavaScript,哪些部分是 TypeScript。
TypeScript is a superset of JavaScript, which means it adds more things to an already existing and defined language. Over time you learn to spot which parts are JavaScript and which parts are TypeScript.
将 TypeScript 视为常规 JavaScript 上的附加类型层确实很有帮助,这是一层薄薄的元信息,在 JavaScript 代码在某个可用运行时中运行之前会被剥离。有些人甚至说 TypeScript 代码在编译后会“擦除为 JavaScript”。
It really helps to see TypeScript as this additional layer of types upon regular JavaScript, a thin layer of metainformation that will be peeled off before your JavaScript code runs in one of the available runtimes. Some people even speak about TypeScript code “erasing to JavaScript” once compiled.
TypeScript 是 JavaScript 之上的这一层,这也意味着不同的语法会贡献不同的层。function或const在 JavaScript 部分创建一个名称,而type声明或interface会在 TypeScript 层中贡献一个名称:
TypeScript being this layer on top of JavaScript also means that different syntax contributes to different layers. While a function or const creates a name in the JavaScript part, a type declaration or an interface contributes a name in the TypeScript layer:
// Collection is in TypeScript land! --> typetypeCollection=Person[]// printCollection is in JavaScript land! --> valuefunctionprintCollection(coll:Collection){console.log(...coll.entries)}
// Collection is in TypeScript land! --> typetypeCollection=Person[]// printCollection is in JavaScript land! --> valuefunctionprintCollection(coll:Collection){console.log(...coll.entries)}
我们还说声明为类型命名空间或值命名空间贡献一个名称。由于类型层位于值层之上,因此可以使用类型层中的值,但反之则不行。我们也有明确的关键字来表示这一点:
We also say that declarations contribute a name to either the type namespace or the value namespace. Since the type layer is on top of the value layer, it’s possible to consume values in the type layer, but not vice versa. We also have explicit keywords for that:
// a valueconstperson={name:"Stefan",};// a typetypePerson=typeofperson;
// a valueconstperson={name:"Stefan",};// a typetypePerson=typeofperson;
typeof从下面的值层创建一个在类型层中可用的名称。
typeof creates a name available in the type layer from the value layer below.
当声明类型同时创建类型和值时,就会变得令人恼火。例如,类可以在 TypeScript 层中用作类型,也可以在 JavaScript 中用作值:
It gets irritating when there are declaration types that create both types and values. Classes, for instance, can be used in the TypeScript layer as a type as well as in JavaScript as a value:
// declarationclassPerson{name:string;constructor(n:string){this.name=n;}}// used as a valueconstperson=newPerson("Stefan");// used as a typetypeCollection=Person[];functionprintPersons(coll:Collection){//...}
// declarationclassPerson{name:string;constructor(n:string){this.name=n;}}// used as a valueconstperson=newPerson("Stefan");// used as a typetypeCollection=Person[];functionprintPersons(coll:Collection){//...}
命名惯例可能会欺骗你。通常,我们用大写首字母来定义类、类型、接口、枚举等。即使它们可能贡献值,它们也肯定会贡献类型。好吧,直到你为你的 React 应用编写大写函数,正如惯例所规定的那样。
And naming conventions can trick you. Usually, we define classes, types, interfaces, enums, and so on with a capital first letter. And even if they may contribute values, they for sure contribute types. Well, until you write uppercase functions for your React app, as the convention dictates.
如果你习惯使用名称作为类型和值,那么当你突然收到一个老套的“TS2749: YourType指的是一个值,但被用作类型”错误时,你就会抓耳挠腮:
If you’re used to using names as types and values, you’re going to scratch your head if you suddenly get a good old “TS2749: YourType refers to a value, but is being used as a type” error:
typePersonProps={name:string;};functionPerson({name}:PersonProps){// ...}typePrintComponentProps={collection:Person[];// ^- 'Person' refers to a value,// but is being used as a type}
typePersonProps={name:string;};functionPerson({name}:PersonProps){// ...}typePrintComponentProps={collection:Person[];// ^- 'Person' refers to a value,// but is being used as a type}
这就是 TypeScript 让人感到困惑的地方。什么是类型,什么是值,为什么我们需要将它们分开,为什么这不像在其他编程语言中那样工作?突然间,你面临着typeof调用甚至InstanceType辅助类型,因为你意识到类实际上贡献了两种类型(参见第 11 章)。
This is where TypeScript can get really confusing. What is a type, what is a value, why do we need to separate them, and why doesn’t this work like in other programming languages? Suddenly, you are confronted with typeof calls or even the InstanceType helper type, because you realize that classes actually contribute two types (see Chapter 11).
类为类型命名空间贡献一个名称,并且由于 TypeScript 是一个结构类型系统,它们允许具有与某个类的实例相同形状的值。因此允许这样做:
Classes contribute a name to the type namespace, and since TypeScript is a structural type system, they allow values that have the same shape as an instance of a certain class. So this is allowed:
classPerson{name:string;constructor(n:string){this.name=n;}}functionprintPerson(person:Person){console.log(person.name);}printPerson(newPerson("Stefan"));// okprintPerson({name:"Stefan"});// also ok
classPerson{name:string;constructor(n:string){this.name=n;}}functionprintPerson(person:Person){console.log(person.name);}printPerson(newPerson("Stefan"));// okprintPerson({name:"Stefan"});// also ok
然而,instanceof完全在值命名空间中进行并且仅在类型命名空间中具有影响的检查将会失败,因为具有相同形状的对象可能具有相同的属性,但不是类的实际实例:
However, instanceof checks, which are working entirely in the value namespace and just have implications in the type namespace, would fail, as objects with the same shape may have the same properties but are not an actual instance of a class:
functioncheckPerson(person:Person){returnpersoninstanceofPerson;}checkPerson(newPerson("Stefan"));// truecheckPerson({name:"Stefan"});// false
functioncheckPerson(person:Person){returnpersoninstanceofPerson;}checkPerson(newPerson("Stefan"));// truecheckPerson({name:"Stefan"});// false
因此,了解哪些内容贡献类型以及哪些内容贡献值非常有用。表 2-1改编自 TypeScript 文档,很好地总结了这一点。
So it’s useful to understand what contributes types and what contributes value. Table 2-1, adapted from the TypeScript docs, sums it up nicely.
| 声明类型 | 类型 | 价值 |
|---|---|---|
班级 Class |
十 X |
十 X |
枚举 Enum |
十 X |
十 X |
界面 Interface |
十 X |
|
类型别名 Type Alias |
十 X |
|
功能 Function |
十 X |
|
多变的 Variable |
十 X |
如果你一开始就坚持使用函数、接口(或类型别名,参见范例 2.5)和变量,你就会知道在什么情况下可以使用什么。如果你使用类,请再考虑一下其含义。
If you stick with functions, interfaces (or type aliases, see Recipe 2.5), and variables at the beginning, you will get a feel for what you can use where. If you work with classes, think about the implications a bit longer.
在上一章中,您了解了使 JavaScript 代码更具表现力的基本构建块。但是,如果您有 JavaScript 经验,您就会明白 TypeScript 的基本类型和注释仅涵盖了其固有灵活性的一小部分。
In the previous chapter you learned about the basic building blocks that allow you to make your JavaScript code more expressive. But if you are experienced in JavaScript, you understand that TypeScript’s fundamental types and annotations cover only a small set of its inherent flexibility.
TypeScript 旨在使 JavaScript 的意图更加清晰,并且它希望在不牺牲灵活性的情况下实现这一点,特别是因为它允许开发人员设计出数百万人使用和喜爱的出色 API。请将 TypeScript 更多地视为一种形式化 JavaScript 的方式,而不是限制它。进入 TypeScript 的类型系统。
TypeScript is supposed to make intentions in JavaScript clearer, and it wants to do so without sacrificing this flexibility, especially since it allowed developers to design fantastic APIs used and loved by millions. Think of TypeScript more as a way to formalize JavaScript, rather than restrict it. Enter TypeScript’s type system.
在本章中,您将开发一个关于如何思考类型的心理模型。您将学习如何根据需要广泛或狭窄地定义值集,以及如何在整个控制流中更改它们的范围。您还将学习如何利用结构类型系统以及何时打破规则。
In this chapter, you will develop a mental model for how to think about types. You will learn how to define sets of values as widely or as narrowly as you need, and how to change their scope throughout your control flow. You will also learn how to leverage a structural type system and when to break with the rules.
本章标志着 TypeScript 基础和高级类型技术之间的界限。但无论您是经验丰富的 TypeScript 开发人员还是刚刚起步,这个思维模型都将成为未来一切的基础。
This chapter marks the line between TypeScript foundations and advanced type techniques. But whether you are an experienced TypeScript developer or just starting out, this mental model will be the baseline for everything to come.
使用联合类型和交集类型来建模数据。使用文字类型来定义 特定变体。
Use union and intersection types to model your data. Use literal types to define specific variants.
假设您正在为一家玩具店创建数据模型。这家玩具店的每件商品都有一些基本属性:名称、数量和建议的最低年龄。其他属性仅与每种特定类型的玩具相关,这需要您创建多个派生项:
Suppose you are creating a data model for a toy shop. Each item in this toy shop has some basic properties: name, quantity, and the recommended minimum age. Additional properties are relevant only for each particular type of toy, which requires you to create several derivations:
typeBoardGame={name:string;price:number;quantity:number;minimumAge:number;players:number;};typePuzzle={name:string;price:number;quantity:number;minimumAge:number;pieces:number;};typeDoll={name:string;price:number;quantity:number;minimumAge:number;material:string;};
typeBoardGame={name:string;price:number;quantity:number;minimumAge:number;players:number;};typePuzzle={name:string;price:number;quantity:number;minimumAge:number;pieces:number;};typeDoll={name:string;price:number;quantity:number;minimumAge:number;material:string;};
对于您创建的函数,您需要一个代表所有玩具的类型,一个仅包含所有玩具所共有的基本属性的超类型:
For the functions you create, you need a type that is representative of all toys, a supertype that contains just the basic properties common to all toys:
typeToyBase={name:string;price:number;quantity:number;minimumAge:number;};functionprintToy(toy:ToyBase){/* ... */}constdoll:Doll={name:"Mickey Mouse",price:9.99,quantity:10000,minimumAge:2,material:"plush",};printToy(doll);// works
typeToyBase={name:string;price:number;quantity:number;minimumAge:number;};functionprintToy(toy:ToyBase){/* ... */}constdoll:Doll={name:"Mickey Mouse",price:9.99,quantity:10000,minimumAge:2,material:"plush",};printToy(doll);// works
这是可行的,因为您可以使用该功能打印所有玩偶、棋盘游戏或拼图,但有一个警告:您会丢失原始玩具的信息printToy。您只能打印通用属性,而不能打印特定属性。
This works, as you can print all dolls, board games, or puzzles with that function, but there’s one caveat: you lose the information of the original toy within printToy. You can print only common properties, not specific ones.
For a type representing all possible toys, you can create a union type:
// Union ToytypeToy=Doll|BoardGame|Puzzle;functionprintToy(toy:Toy){/* ... */}
// Union ToytypeToy=Doll|BoardGame|Puzzle;functionprintToy(toy:Toy){/* ... */}
将类型视为一组兼容值是一种好方法。对于每个值(无论是否注释),TypeScript 都会检查此值是否与某种类型兼容。对于对象,这还包括具有比其类型中定义的更多属性的值。通过推断,具有更多属性的值在结构类型系统中被分配一个子类型。子类型的值也是超类型集的一部分。
A good way to think of a type is as a set of compatible values. For each value, either annotated or not, TypeScript checks if this value is compatible with a certain type. For objects, this also includes values with more properties than defined in their type. Through inference, values with more properties are assigned a subtype in the structural type system. And values of subtypes are also part of the supertype set.
联合类型是集合的联合。兼容值的数量越来越广泛,并且类型之间也存在一些重叠。例如,一个同时具有
material和 的对象可以与和players兼容。这是一个需要注意的细节,您可以在方案 3.2中看到处理该细节的方法。DollBoardGame
A union type is a union of sets. The number of compatible values gets broader, and there is also some overlap between types. For example, an object that has both
material and players can be compatible with both Doll and BoardGame. This is a detail to look out for, and you can see a method to work with that detail in Recipe 3.2.
图 3-1以维恩图的形式说明了联合类型的概念。集合论类比在这里也很好用。
Figure 3-1 illustrates the concept of a union type in the form of a Venn diagram. Set theory analogies work well here, too.
您可以在任何地方创建联合类型,并且可以使用原始类型:
You can create union types everywhere, and with primitive types:
functiontakesNumberOrString(value:number|string){/* ... */}takesNumberOrString(2);// oktakesNumberOrString("Hello");// ok
functiontakesNumberOrString(value:number|string){/* ... */}takesNumberOrString(2);// oktakesNumberOrString("Hello");// ok
这使您可以根据需要扩大值集。
This allows you to widen the set of values as much as you like.
您在玩具店示例中还会看到一些冗余:属性重复。如果我们可以将其用作每个并集部分的基础,ToyBase那就更好了。我们可以使用交集类型:ToyBase
What you also see in the toy shop example is some redundancy: the ToyBase properties are repeated. It would be much nicer if we could use ToyBase as the basis of each union part. And we can, using intersection types:
typeToyBase={name:string;price:number;quantity:number;minimumAge:number;};// Intersection of ToyBase and { players: number }typeBoardGame=ToyBase&{players:number;};// Intersection of ToyBase and { pieces: number }typePuzzle=ToyBase&{pieces:number;};// Intersection of ToyBase and { material: string }typeDoll=ToyBase&{material:string;};
typeToyBase={name:string;price:number;quantity:number;minimumAge:number;};// Intersection of ToyBase and { players: number }typeBoardGame=ToyBase&{players:number;};// Intersection of ToyBase and { pieces: number }typePuzzle=ToyBase&{pieces:number;};// Intersection of ToyBase and { material: string }typeDoll=ToyBase&{material:string;};
就像联合类型一样,交集类型与集合论中的对应类型类似。它们告诉 TypeScript,兼容的值需要为类型A 和类型B。该类型现在接受一组更窄的值,其中包括两种类型的所有属性,包括它们的子类型。图 3-2显示了交集类型的可视化。
Just like union types, intersection types resemble their counterparts from set theory. They tell TypeScript that compatible values need to be of type A and type B. The type now accepts a narrower set of values, one that includes all properties from both types, including their subtypes. Figure 3-2 shows a visualization of an intersection type.
交集类型也适用于原始类型,但它们没有什么用处。 的交集string & number导致never,因为没有值同时满足string和number属性。
Intersection types also work on primitive types, but they are of no good use. An intersection of string & number results in never, as no value satisfies both string and number properties.
除了类型别名和交集类型,您还可以使用接口来定义模型。在方案 2.5中,我们讨论了它们之间的区别,您需要注意一些区别。因此,atype BoardGame = ToyBase & { /* ... */ }可以很容易地描述为interface BoardGame extends ToyBase { /* ... */ }。但是,您不能定义联合类型的接口。不过,您可以定义接口的联合。
Instead of type aliases and intersection types you can also define your models with interfaces. In Recipe 2.5 we talk about the differences between them, and there are a few you need to look out for. So a type BoardGame = ToyBase & { /* ... */ } can easily be described as interface BoardGame extends ToyBase { /* ... */ }. However, you can’t define an interface that is a union type. You can define a union of interfaces, though.
这些已经是在 TypeScript 中建模数据的好方法了,但我们可以做得更多。在TypeScript,文字值可以表示为文字类型。我们可以定义一个类型,例如数字 1,唯一兼容的值是1:
These are already great ways to model data within TypeScript, but we can do a little more. In TypeScript, literal values can be represented as a literal type. We can define a type that is just, for example, the number 1, and the only compatible value is 1:
typeOne=1;constone:One=1;// nothing else can be assigned.
typeOne=1;constone:One=1;// nothing else can be assigned.
这被称为文字类型,虽然它单独看起来似乎没什么用,但当你将多个文字类型组合成一个联合体时,它就非常有用了。Doll例如,对于类型,我们可以明确设置允许的值material:
This is called a literal type, and while it doesn’t seem to be quite useful alone, it is of great use when you combine multiple literal types to a union. For the Doll type, for example, we can explicitly set allowed values for material:
typeDoll=ToyBase&{material:"plush"|"plastic";};functioncheckDoll(doll:Doll){if(doll.material==="plush"){// do something with plush}else{// doll.material is "plastic", there are no other options}}
typeDoll=ToyBase&{material:"plush"|"plastic";};functioncheckDoll(doll:Doll){if(doll.material==="plush"){// do something with plush}else{// doll.material is "plastic", there are no other options}}
这使得分配除"plush"或之外的任何值都"plastic"不可能,并使我们的代码更加健壮。
This makes assigning any value other than "plush" or "plastic" impossible and makes our code much more robust.
有了联合类型、交集类型和文字类型,定义更复杂的模型变得更加容易。
With union types, intersection types, and literal types, it becomes much easier to define even elaborate models.
为每个联合部分添加一个kind具有字符串文字类型的属性,并检查其内容。
Add a kind property to each union part with a string literal type, and check for its contents.
让我们看一个类似于我们在3.1 节中创建的数据模型。这次,我们想为图形软件定义各种形状:
Let’s look at a data model similar to what we created in Recipe 3.1. This time, we want to define various shapes for a graphics software:
typeCircle={radius:number;};typeSquare={x:number;};typeTriangle={x:number;y:number;};typeShape=Circle|Triangle|Square;
typeCircle={radius:number;};typeSquare={x:number;};typeTriangle={x:number;y:number;};typeShape=Circle|Triangle|Square;
这些类型之间有一些相似之处,但仍然有足够的信息来在area函数中区分它们:
There are some similarities between the types but there is also still enough information to differentiate between them in an area function:
functionarea(shape:Shape){if("radius"inshape){// shape is CirclereturnMath.PI*shape.radius*shape.radius;}elseif("y"inshape){// shape is Trianglereturn(shape.x*shape.y)/2;}else{// shape is Squarereturnshape.x*shape.x;}}
functionarea(shape:Shape){if("radius"inshape){// shape is CirclereturnMath.PI*shape.radius*shape.radius;}elseif("y"inshape){// shape is Trianglereturn(shape.x*shape.y)/2;}else{// shape is Squarereturnshape.x*shape.x;}}
这可以行得通,但有一些注意事项。虽然Circle是唯一具有 属性的类型radius,Triangle并且Square共享x属性。由于Square仅由x属性组成,因此这使其成为Triangle的子类型Square。
This works, but it comes with a few caveats. While Circle is the only type with a radius property, Triangle and Square share the x property. Since Square consists only of the x property, this makes Triangle a subtype of Square.
考虑到我们如何定义控制流以首先检查区分子类型属性y,这不是问题,但单独检查并在控制流中创建一个以相同方式计算和x面积的分支太容易了,这是错误的。TriangleSquare
Given how we defined the control flow to check for the distinguishing subtype property y first, this is not an issue, but it’s just too easy to check for x alone and create a branch in the control flow that computes the area for both Triangle and Square in the same manner, which is just wrong.
扩展 也很困难Shape。如果我们查看矩形所需的属性,我们会发现它包含与 相同的属性Triangle:
It is also hard to extend Shape. If we look at the required properties for a rectangle, we see that it contains the same properties as Triangle:
typeRectangle={x:number;y:number;};typeShape=Circle|Triangle|Square|Rectangle;
typeRectangle={x:number;y:number;};typeShape=Circle|Triangle|Square|Rectangle;
没有明确的方法来区分 union 的各个部分。为了确保 union 的各个部分都可以区分,我们需要使用识别属性来扩展我们的模型,以便绝对清楚地说明我们正在处理什么。
There is no clear way to differentiate between each part of a union. To make sure each part of a union is distinguishable, we need to extend our models with an identifying property that makes absolutely clear what we are dealing with.
这可以通过添加属性来实现kind。此属性采用字符串文字类型来标识模型的一部分。
This can happen through the addition of a kind property. This property takes a string literal type identifying the part of the model.
如范例 3.1所示,TypeScript 允许你将诸如、、和之类的原始类型子集化为具体string值。这意味着每个值也是一种类型,即一个由一个兼容值组成的集合。numberbigintboolean
As seen in Recipe 3.1, TypeScript allows you to subset primitive types like string, number, bigint, and boolean to concrete values. Which means that every value is also a type, a set that consists of exactly one compatible value.
因此,为了明确定义我们的模型,我们kind为每个模型部分添加一个属性,并将其设置为识别该部分的精确文字类型:
So for our model to be clearly defined, we add a kind property to each model part and set it to an exact literal type identifying this part:
typeCircle={radius:number;kind:"circle";};typeSquare={x:number;kind:"square";};typeTriangle={x:number;y:number;kind:"triangle";};typeShape=Circle|Triangle|Square;
typeCircle={radius:number;kind:"circle";};typeSquare={x:number;kind:"square";};typeTriangle={x:number;y:number;kind:"triangle";};typeShape=Circle|Triangle|Square;
请注意,我们没有设置kind为,string而是设置为精确的文字类型"circle"(或"square"和"triangle")。这是一种类型,而不是值,但唯一兼容的值是文字字符串。
Note that we don’t set kind to string but to the exact literal type "circle" (or "square" and "triangle", respectively). This is a type, not a value, but the only compatible value is the literal string.
添加kind字符串文字类型的属性可确保联合体各部分之间不会有任何重叠,因为文字类型彼此不兼容。这种技术称为可区分联合类型,可有效地将联合体类型的每个集合分离出来Shape,指向一个精确的集合。
Adding the kind property with string literal types ensures there can’t be any overlap between parts of the union, as the literal types are not compatible with one another. This technique is called discriminated union types and effectively tears away each set that’s part of the union type Shape, pointing to an exact set.
这对于函数来说非常棒area,因为我们可以有效地区分,例如在一条switch语句中:
This is fantastic for the area function, as we can effectively distinguish, for example, in a switch statement:
functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:throwError("not possible");}}
functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:throwError("not possible");}}
这不仅使我们对所要处理的事情有了非常清晰的认识,而且对于即将发生的变化也具有很强的未来性,正如我们将在方案 3.3中看到的那样。
Not only does it become absolutely clear what we are dealing with, but it is also very future proof to upcoming changes, as we will see in Recipe 3.3.
创建详尽性检查,断言所有剩余的情况都不会在assertNever函数中发生。
Create exhaustiveness checks where you assert that all remaining cases can never happen with an assertNever function.
让我们看一下方案 3.2中的完整示例:
Let’s look at the full example from Recipe 3.2:
typeCircle={radius:number;kind:"circle";};typeSquare={x:number;kind:"square";};typeTriangle={x:number;y:number;kind:"triangle";};typeShape=Circle|Triangle|Square;functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:throwError("not possible");}}
typeCircle={radius:number;kind:"circle";};typeSquare={x:number;kind:"square";};typeTriangle={x:number;y:number;kind:"triangle";};typeShape=Circle|Triangle|Square;functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:throwError("not possible");}}
使用可区分联合,我们可以区分联合的各个部分。该area函数使用 switch-case 语句分别处理每个情况。由于kind属性的字符串文字类型,因此类型之间不会重叠。
Using discriminated unions, we can distinguish between each part of a union. The area function uses a switch-case statement to handle each case separately. Thanks to string literal types for the kind property, there can be no overlap between types.
一旦所有选项都用尽,默认情况下我们会抛出一个错误,表明我们遇到了不应该发生的无效情况。如果我们的类型在整个代码库中都是正确的,那么就不应该抛出这个错误。
Once all options are exhausted, in the default case we throw an error, indicating that we reached an invalid situation that should never occur. If our types are right throughout the codebase, this error should never be thrown.
甚至类型系统也告诉我们默认情况是不可能发生的情况。如果我们添加默认情况shape并将鼠标悬停在其上,TypeScript 会告诉我们其shape类型为never:
Even the type system tells us that the default case is an impossible scenario. If we add shape in the default case and hover over it, TypeScript tells us that shape is of type never:
functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:console.error("Shape not defined:",shape);// shape is neverthrowError("not possible");}}
functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:console.error("Shape not defined:",shape);// shape is neverthrowError("not possible");}}
never是一种有趣的类型。它是 TypeScript底层类型,这意味着它位于类型层次结构的最末端。其中any和unknown包括所有可能的值,没有值与 兼容never。它是空集,这解释了名称的含义。如果您的一个值恰好是 类型,那么您将处于一种永远不会never发生的情况。
never is an interesting type. It’s TypeScript bottom type, meaning that it’s at the very end of the type hierarchy. Where any and unknown include every possible value, no value is compatible to never. It’s the empty set, which explains the name. If one of your values happens to be of type never, you are in a situation that should never
happen.
shape如果我们用以下方式扩展类型,则默认情况下的类型会立即改变Shape,例如Rectangle:
The type of shape in the default cases changes immediately if we extend the type Shape with, for example, a Rectangle:
typeRectangle={x:number;y:number;kind:"rectangle";};typeShape=Circle|Triangle|Square|Rectangle;functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:console.error("Shape not defined:",shape);// shape is RectanglethrowError("not possible");}}
typeRectangle={x:number;y:number;kind:"rectangle";};typeShape=Circle|Triangle|Square|Rectangle;functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:console.error("Shape not defined:",shape);// shape is RectanglethrowError("not possible");}}
这是控制流分析的最佳表现:TypeScript 在每个时间点都准确地知道您的值具有哪些类型。在分支中default,shape是类型
Rectangle,但我们希望处理矩形。如果 TypeScript 可以告诉我们我们错过了处理潜在类型,那不是很好吗?随着这一变化,我们现在每次计算矩形的形状时都会遇到它。默认情况旨在处理(从类型系统的角度来看)不可能的情况;我们希望保持这种状态。
This is control flow analysis at its best: TypeScript knows at exactly every point in time which types your values have. In the default branch, shape is of type
Rectangle, but we are expected to deal with rectangles. Wouldn’t it be great if TypeScript could tell us that we missed taking care of a potential type? With the change, we now run into it every time we calculate the shape of a rectangle. The default case was meant to handle (from the perspective of the type system) impossible situations; we’d like to keep it that way.
在某种情况下,这种情况已经很糟糕了,如果你在代码库中多次使用详尽性检查模式,情况会变得更糟。你无法确定你没有错过软件最终会崩溃的某个地方。
This is already bad in one situation, and it gets worse if you use the exhaustiveness checking pattern multiple times in your codebase. You can’t tell for sure that you didn’t miss one spot where your software will ultimately crash.
确保处理所有可能情况的一种方法是创建一个辅助函数,该函数断言所有选项都已用尽。它应确保唯一可能的值是没有值:
One technique to ensure that you handled all possible cases is to create a helper function that asserts that all options are exhausted. It should ensure that the only values possible are no values:
functionassertNever(value:never){console.error("Unknown value",value);throwError("Not possible");}
functionassertNever(value:never){console.error("Unknown value",value);throwError("Not possible");}
通常,你会看到never一个迹象,表明你处于一个不可能的境地。在这里,我们将其用作函数签名的显式类型注释。你可能会问:我们应该传递哪些值?答案是:没有!在最好的情况下,这个函数永远不会被调用。
Usually, you see never as an indicator that you are in an impossible situation. Here, we use it as an explicit type annotation for a function signature. You might ask: which values are we supposed to pass? And the answer is: none! In the best case, this function will never get called.
但是,如果我们用 替换示例中的原始默认值assertNever,则可以使用类型系统来确保所有可能的值都是兼容的,即使没有值:
However, if we substitute the original default case from our example with assertNever, we can use the type system to ensure that all possible values are compatible, even if there are no values:
functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:// shape is RectangleassertNever(shape);// ^-- Error: Argument of type 'Rectangle' is not// assignable to parameter of type 'never'}}
functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;default:// shape is RectangleassertNever(shape);// ^-- Error: Argument of type 'Rectangle' is not// assignable to parameter of type 'never'}}
太棒了!现在,只要我们忘记用尽所有选项,就会出现红色波浪线。TypeScript 编译此代码时会出现错误,并且很容易在我们的代码库中找到需要添加以下Rectangle情况的所有情况:
Great! We now get red squiggly lines whenever we forget to exhaust all options. TypeScript won’t compile this code without an error, and it’s easy to spot all occurrences in our codebase where we need to add the Rectangle case:
functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;case"rectangle":returnshape.x*shape.y;default:// shape is neverassertNever(shape);// shape can be passed to assertNever!}}
functionarea(shape:Shape){switch(shape.kind){case"circle":// shape is CirclereturnMath.PI*shape.radius*shape.radius;case"triangle":// shape is Trianglereturn(shape.x*shape.y)/2;case"square":// shape is Squarereturnshape.x*shape.x;case"rectangle":returnshape.x*shape.y;default:// shape is neverassertNever(shape);// shape can be passed to assertNever!}}
尽管never没有兼容值,并且用于指示(对于类型系统而言)不可能的情况,但我们可以使用类型作为类型注释,以确保我们不会忘记可能的情况。将类型视为兼容值的集合,这些值可以根据控制流变宽或变窄,这让我们想到了诸如这样的技术assertNever,这是一个非常有用的小函数,可以增强我们代码库的质量。
Even though never has no compatible values and is used to indicate—for the type system—an impossible situation, we can use the type as type annotation to make sure we don’t forget about possible situations. Seeing types as sets of compatible values that can get broader or narrower based on control flow leads us to techniques like assertNever, a very helpful little function that can strengthen our codebase’s quality.
使用类型断言和const context固定字面量的类型。
Pin the type of your literals using type assertions and const context.
在 TypeScript 中,可以将每个值用作其自己的类型。这些称为文字类型,允许您将较大的集合子集设置为几个有效值。
In TypeScript, it’s possible to use each value as its own type. These are called literal types and allow you to subset bigger sets to just a couple of valid values.
TypeScript 中的文字类型不仅是指向特定值的一个好技巧,也是类型系统工作方式的重要组成部分。当您通过let或将原始类型的值分配给不同的绑定时,这一点变得显而易见const。
Literal types in TypeScript are not only a nice trick to point to specific values but are also an essential part of how the type system works. This becomes obvious when you assign values of primitive types to different bindings via let or const.
如果我们两次分配相同的值,一次通过let,一次通过const,TypeScript 会推断出两种不同的类型。通过let绑定,TypeScript 将推断出更广泛的原始类型:
If we assign the same value twice, once via let and once via const, TypeScript infers two different types. With the let binding, TypeScript will infer the broader primitive type:
letname="Stefan";// name is string
letname="Stefan";// name is string
通过const绑定,TypeScript 将推断出准确的文字类型:
With a const binding, TypeScript will infer the exact literal type:
constname="Stefan";// name is "Stefan"
constname="Stefan";// name is "Stefan"
对象类型的行为略有不同。let绑定仍然可以推断出更广泛的集合:
Object types behave slightly differently. let bindings still infer the broader set:
// person is { name: string }letperson={name:"Stefan"};
// person is { name: string }letperson={name:"Stefan"};
但const绑定也是如此:
But so do const bindings:
// person is { name: string }constperson={name:"Stefan"};
// person is { name: string }constperson={name:"Stefan"};
这背后的原因是在 JavaScript 中,虽然绑定本身是常量,这意味着我无法重新分配person,但对象属性的值可以改变:
The reasoning behind this is in JavaScript, while the binding itself is constant, which means I can’t reassign person, the values of an object’s property can change:
// person is { name: string }constperson={name:"Stefan"};person.name="Not Stefan";// works!
// person is { name: string }constperson={name:"Stefan"};person.name="Not Stefan";// works!
这种行为在某种意义上是正确的,因为它反映了 JavaScript 的行为,但当我们对数据模型非常精确时,它可能会导致问题。
This behavior is correct in the sense that it mirrors the behavior of JavaScript, but it can cause problems when we are very exact with our data models.
在之前的配方中,我们使用联合类型和交集类型对数据进行建模。我们使用可区分的联合类型来区分过于相似的类型。
In the previous recipes we modeled data using union and intersection types. We used discriminated union types to distinguish between types that are too similar.
问题是,当我们使用文字作为数据时,TypeScript 通常会推断出更广泛的集合,这会导致值与定义的类型不兼容。这会产生非常长的错误消息:
The problem is that when we use literals for data, TypeScript will usually infer the broader set, which makes the values incompatible to the types defined. This produces a very lengthy error message:
typeCircle={radius:number;kind:"circle";};typeSquare={x:number;kind:"square";};typeTriangle={x:number;y:number;kind:"triangle";};typeShape=Circle|Triangle|Square;functionarea(shape:Shape){/* ... */}constcircle={radius:2,kind:"circle",};area(circle);// ^-- Argument of type '{ radius: number; kind: string; '// is not assignable to parameter of type 'Shape'.// Type '{ radius: number; kind: string; }' is not// assignable to type 'Circle'.// Types of property 'kind' are incompatible.// Type 'string' is not assignable to type '"circle"'.
typeCircle={radius:number;kind:"circle";};typeSquare={x:number;kind:"square";};typeTriangle={x:number;y:number;kind:"triangle";};typeShape=Circle|Triangle|Square;functionarea(shape:Shape){/* ... */}constcircle={radius:2,kind:"circle",};area(circle);// ^-- Argument of type '{ radius: number; kind: string; '// is not assignable to parameter of type 'Shape'.// Type '{ radius: number; kind: string; }' is not// assignable to type 'Circle'.// Types of property 'kind' are incompatible.// Type 'string' is not assignable to type '"circle"'.
有几种方法可以解决这个问题。首先,我们可以使用显式注释来确保类型。如方案 2.1所述,每个注释都是一个类型检查,这意味着检查右侧的值是否兼容。由于没有推断,Typescript 将查看确切的值来确定对象文字是否兼容:
There are several ways to solve this problem. First, we can use explicit annotations to ensure the type. As described in Recipe 2.1, each annotation is a type-check, which means the value on the righthand side is checked for compatibility. Since there is no inference, Typescript will look at the exact values to decide whether an object literal is compatible:
// Exact typeconstcircle:Circle={radius:2,kind:"circle",};area(circle);// Works!// Broader setconstcircle:Shape={radius:2,kind:"circle",};area(circle);// Also works!
// Exact typeconstcircle:Circle={radius:2,kind:"circle",};area(circle);// Works!// Broader setconstcircle:Shape={radius:2,kind:"circle",};area(circle);// Also works!
除了类型注释之外,我们还可以在赋值结束时进行类型断言 :
Instead of type annotations, we can also do type assertions at the end of the assignment:
// Type assertionconstcircle={radius:2,kind:"circle",}asCircle;area(circle);// Works!
// Type assertionconstcircle={radius:2,kind:"circle",}asCircle;area(circle);// Works!
但有时,注释也会限制我们。尤其是当我们必须处理包含更多信息且在不同位置使用且具有不同语义的文字时,情况尤其如此。
Sometimes, however, annotations can limit us. This is true especially when we have to work with literals that contain more information and are used in different places with different semantics.
从我们注释或断言为的那一刻起Circle,绑定将始终是一个圆圈,无论circle实际携带哪些值。
From the moment we annotate or assert as Circle, the binding will always be a circle, no matter which values circle actually carries.
但是,我们可以对断言进行更细粒度的处理。我们不必断言整个对象属于某种类型,而是可以断言单个属性属于某种类型:
But we can be much more fine-grained with assertions. Instead of asserting that the entire object is of a certain type, we can assert single properties to be of a certain type:
constcircle={radius:2,kind:"circle"as"circle",};area(circle);// Works!
constcircle={radius:2,kind:"circle"as"circle",};area(circle);// Works!
断言为精确值的另一种方法是使用带有类型断言的const 上下文as const;TypeScript 将值锁定为文字类型:
Another way to assert as exact values is to use const context with an as const type assertion; TypeScript locks the value in as literal type:
constcircle={radius:2,kind:"circle"asconst,};area(circle);// Works!
constcircle={radius:2,kind:"circle"asconst,};area(circle);// Works!
如果我们将const 上下文应用于整个对象,我们还可以确保值是只读的并且不会被更改:
If we apply const context to the entire object, we also make sure that the values are read-only and won’t be changed:
constcircle={radius:2,kind:"circle",}asconst;area2(circle);// Works!circle.kind="rectangle";// ^-- Cannot assign to 'kind' because// it is a read-only property.
constcircle={radius:2,kind:"circle",}asconst;area2(circle);// Works!circle.kind="rectangle";// ^-- Cannot assign to 'kind' because// it is a read-only property.
如果我们想将值固定为其确切的字面类型并保持这种状态,那么const 上下文类型断言是一个非常方便的工具。如果您的代码库中有很多对象字面量不应该改变,但需要在各种场合使用,那么const 上下文可以提供帮助!
Const context type assertions are a very handy tool if we want to pin values to their exact literal type and keep them that way. If there are a lot of object literals in your code base that are not supposed to change but need to be consumed in various occasions, const context can help!
将类型谓词添加到辅助函数的签名中,以指示布尔条件对类型系统的影响。
Add type predicates to a helper function’s signature to indicate the impact of a Boolean condition for the type system.
使用文字类型和联合类型,TypeScript 允许您定义非常具体的值集。例如,我们可以轻松定义一个有六面的骰子:
With literal types and union types, TypeScript allows you to define very specific sets of values. For example, we can define a die with six sides easily:
typeDice=1|2|3|4|5|6;
typeDice=1|2|3|4|5|6;
虽然这种符号很有表现力,并且类型系统可以准确地告诉您哪些值是有效的,但要获得这种类型还需要一些工作。
While this notation is expressive, and the type system can tell you exactly which values are valid, it requires some work to get to this type.
假设我们有某种游戏,用户可以输入任意数字。如果输入的数字是有效的点数,我们就会执行某些操作。
Let’s imagine we have some kind of game where users are allowed to input any number. If it’s a valid number of dots, we are doing certain actions.
我们编写一个条件检查来查看输入的数字是否是一组值的一部分:
We write a conditional check to see if the input number is part of a set of values:
functionrollDice(input:number){if([1,2,3,4,5,6].includes(input)){// `input` is still `number`, even though we know it// should be Dice}}
functionrollDice(input:number){if([1,2,3,4,5,6].includes(input)){// `input` is still `number`, even though we know it// should be Dice}}
问题是,即使我们进行了检查以确保值集已知,TypeScript 仍会将其处理input为number。类型系统无法将你的检查与类型系统的更改联系起来。
The problem is that even though we do a check to make sure the set of values is known, TypeScript still handles input as number. There is no way for the type system to make the connection between your check and the change in the type system.
但你可以帮助类型系统。首先,将你的检查提取到它自己的辅助 函数中:
But you can help the type system. First, extract your check into its own helper function:
functionisDice(value:number):boolean{return[1,2,3,4,5,6].includes(value);}
functionisDice(value:number):boolean{return[1,2,3,4,5,6].includes(value);}
请注意,此检查返回一个boolean。此条件要么为真,要么为假。对于返回布尔值的函数,我们可以将函数签名的返回类型更改为类型谓词。
Note that this check returns a boolean. Either this condition is true or it’s false. For functions that return a Boolean value, we can change the return type of the function signature to a type predicate.
我们告诉 TypeScript,如果此函数返回 true,我们将更多地了解传递给该函数的值。在我们的例子中,value是类型Dice:
We tell TypeScript that if this function returns true, we know more about the value that has been passed to the function. In our case, value is of type Dice:
functionisDice(value:number):valueisDice{return[1,2,3,4,5,6].includes(value);}
functionisDice(value:number):valueisDice{return[1,2,3,4,5,6].includes(value);}
这样,TypeScript 就会提示你的值的实际类型,从而允许你对值进行更细粒度的操作:
With that, TypeScript gets a hint of what the actual types of your values are, allowing you to do more fine-grained operations on your values:
functionrollDice(input:number){if(isDice(input)){// Great! `input` is now `Dice`}else{// input is still `number`}}
functionrollDice(input:number){if(isDice(input)){// Great! `input` is now `Dice`}else{// input is still `number`}}
TypeScript 具有限制性,不允许使用类型谓词进行任何断言。它必须是比原始类型更窄的类型。例如,获取输入string并断言子集number作为输出将出错:
TypeScript is restrictive and doesn’t allow any assertion with type predicates. It needs to be a type that is narrower than the original type. For example, getting a string input and asserting a subset of number as output will error:
typeDice=1|2|3|4|5|6;functionisDice(value:string):valueisDice{// Error: A type predicate's type must be assignable to// its parameter's type. Type 'number' is not assignable to type 'string'.return["1","2","3","4","5","6"].includes(value);}
typeDice=1|2|3|4|5|6;functionisDice(value:string):valueisDice{// Error: A type predicate's type must be assignable to// its parameter's type. Type 'number' is not assignable to type 'string'.return["1","2","3","4","5","6"].includes(value);}
这种故障安全机制在类型层面上为您提供了一些保证,但有一个警告:它不会检查您的条件是否合理。原始检查isDice确保传递的值包含在有效数字数组中。
This fail-safe mechanism gives you some guarantee on the type level, but there is a caveat: it won’t check if your conditions make sense. The original check in isDice ensures that the value passed is included in an array of valid numbers.
此数组中的值由您选择。如果您包含错误的数字,即使您的检查没有对齐,TypeScript 仍会认为value是有效的:Dice
The values in this array are your choice. If you include a wrong number, TypeScript will still think value is a valid Dice, even though your check does not line up:
// Correct on a type-level// incorrect set of values on a value-levelfunctionisDice(value:number):valueisDice{return[1,2,3,4,5,7].includes(value);}
// Correct on a type-level// incorrect set of values on a value-levelfunctionisDice(value:number):valueisDice{return[1,2,3,4,5,7].includes(value);}
这很容易被绊倒。示例 3-1中的条件对于整数来说是正确的,但是如果传递的是浮点数,则不正确。例如,3.1415将是有效的Dice点数!
This is easy to trip over. The condition in Example 3-1 is true for integer numbers but wrong if you pass a floating point number. For example, 3.1415 would be a valid Dice dot count!
isDice浮点数的错误逻辑// Correct on a type-level, incorrect logicfunctionisDice(value:number):valueisDice{returnvalue>=1&&value<=6;}
// Correct on a type-level, incorrect logicfunctionisDice(value:number):valueisDice{returnvalue>=1&&value<=6;}
实际上,任何条件都适用于 TypeScript。Returntrue和 TypeScript 会认为value是Dice:
Actually, any condition works for TypeScript. Return true and TypeScript will think value is Dice:
functionisDice(value:number):valueisDice{returntrue;}
functionisDice(value:number):valueisDice{returntrue;}
TypeScript 将类型断言交到您的手中。您有责任确保这些断言有效且合理。如果您严重依赖通过类型谓词进行的类型断言,请确保进行相应的测试。
TypeScript puts type assertions in your hand. It is your duty to make sure those assertions are valid and sound. If you rely heavily on type assertions via type predicates, make sure that you test accordingly.
拥抱void作为回调的可替代类型。
Embrace void as a substitutable type for callbacks.
您可能从 Java 或 C# 等编程语言中了解到void,它表示没有返回值。void也存在于 TypeScript 中,乍一看它做同样的事情:如果您的函数或方法没有返回任何内容,则返回类型为void。
You might know void from programming languages like Java or C#, where it indicates the absence of a return value. void also exists in TypeScript, and at first glance it does the same thing: if your functions or methods aren’t returning something, the return type is void.
然而,再看一眼, 的行为void就更加微妙了,它在类型系统中的位置也是如此。 voidTypeScript 中的 是 的子类型undefined。 JavaScript 中的函数总是返回某个值。 函数要么显式返回一个值,要么隐式返回undefined:
At second glance, however, the behavior of void is a bit more nuanced, and so is its position in the type system. void in TypeScript is a subtype of undefined. Functions in JavaScript always return something. Either a function explicitly returns a value, or it implicitly returns undefined:
functioniHaveNoReturnValue(i){console.log(i);}letcheck=iHaveNoReturnValue(2);// check is undefined
functioniHaveNoReturnValue(i){console.log(i);}letcheck=iHaveNoReturnValue(2);// check is undefined
如果我们为 创建一个类型iHaveNoReturnValue,它将显示一个void具有返回类型的函数类型:
If we created a type for iHaveNoReturnValue, it would show a function type with void as return type:
functioniHaveNoReturnValue(i){console.log(i);}typeFn=typeofiHaveNoReturnValue;// type Fn = (i: any) => void
functioniHaveNoReturnValue(i){console.log(i);}typeFn=typeofiHaveNoReturnValue;// type Fn = (i: any) => void
voidas 类型也可用于参数和所有其他声明。唯一可以传递的值是undefined:
void as type can also be used for parameters and all other declarations. The only value that can be passed is undefined:
functioniTakeNoParameters(x:void):void{}iTakeNoParameters();// worksiTakeNoParameters(undefined);// worksiTakeNoParameters(void2);// works
functioniTakeNoParameters(x:void):void{}iTakeNoParameters();// worksiTakeNoParameters(undefined);// worksiTakeNoParameters(void2);// works
void和undefined几乎相同。但有一个显著的区别:void返回类型可以用不同类型替换,以允许高级回调模式。fetch例如,让我们创建一个函数。它的任务是获取一组数字并将结果传递给回调函数,作为参数提供:
void and undefined are pretty much the same. There’s one significant difference though: void as a return type can be substituted with different types, to allow for advanced callback patterns. Let’s create a fetch function, for example. Its task is to get a set of numbers and pass the results to a callback function, provided as a parameter:
functionfetchResults(callback:(statusCode:number,results:number[])=>void){// get results from somewhere ...callback(200,results);}
functionfetchResults(callback:(statusCode:number,results:number[])=>void){// get results from somewhere ...callback(200,results);}
回调函数的签名中有两个参数:状态码和结果,返回类型为void。我们可以fetchResults使用与类型完全匹配的回调函数进行调用callback:
The callback function has two parameters in its signature—a status code and the results—and the return type is void. We can call fetchResults with callback functions that match the exact type of callback:
functionnormalHandler(statusCode:number,results:number[]):void{// do something with both parameters}fetchResults(normalHandler);
functionnormalHandler(statusCode:number,results:number[]):void{// do something with both parameters}fetchResults(normalHandler);
但是如果函数类型指定了返回类型void,那么具有不同、更具体的返回类型的函数也会被接受:
But if a function type specifies return type void, functions with a different, more specific return type are also accepted:
functionhandler(statusCode:number):boolean{// evaluate the status code ...returntrue;}fetchResults(handler);// compiles, no problem!
functionhandler(statusCode:number):boolean{// evaluate the status code ...returntrue;}fetchResults(handler);// compiles, no problem!
函数签名不完全匹配,但代码仍可编译。首先,在函数签名中提供较短的参数列表是可以的。JavaScript 可以调用带有多余参数的函数,如果函数中未指定这些参数,则会被忽略。无需携带超出实际需要的参数。
The function signatures don’t match exactly, but the code still compiles. First, it’s OK to provide functions with a shorter argument list in their signature. JavaScript can call functions with excess parameters, and if they aren’t specified in the function, they’re simply ignored. No need to carry more parameters than you actually need.
其次,返回类型是boolean,但 TypeScript 仍会传递此函数。这在声明void返回类型时很有用。原始调用者fetchResults在调用回调时不期望返回值。因此对于类型系统来说, 的返回值callback仍然是undefined,即使它可能是其他值。
Second, the return type is boolean, but TypeScript will still pass this function along. This is useful when declaring a void return type. The original caller fetchResults does not expect a return value when calling the callback. So for the type system, the return value of callback is still undefined, even though it could be something else.
只要类型系统不允许您使用返回值,您的代码就应该是安全的:
As long as the type system won’t allow you to work with the return value, your code should be safe:
functionfetchResults(callback:(statusCode:number,results:number[])=>void){// get results from somewhere ...constdidItWork=callback(200,results);// didItWork is `undefined` in the type system,// even though it would be a boolean with `handler`.}
functionfetchResults(callback:(statusCode:number,results:number[])=>void){// get results from somewhere ...constdidItWork=callback(200,results);// didItWork is `undefined` in the type system,// even though it would be a boolean with `handler`.}
这就是为什么我们可以传递具有任何返回类型的回调。即使回调返回了某些内容,该值也不会被使用,而是进入 void。
That’s why we can pass callbacks with any return type. Even if the callback returns something, this value isn’t used and goes into the void.
权力在于调用函数,它最清楚回调函数会返回什么。如果调用函数根本不需要回调函数的返回值,那么一切皆有可能!
The power lies within the calling function, which knows best what to expect from the callback function. And if the calling function doesn’t require a return value at all from the callback, anything goes!
TypeScript 将此功能称为可替代性:在有意义的地方用一个东西替代另一个东西的能力。乍一看这可能看起来很奇怪。但特别是当你使用不是你编写的库时,你会发现这个功能非常有价值。
TypeScript calls this feature substitutability: the ability to substitute one thing for another, wherever it makes sense. This might seem odd at first. But especially when you work with libraries that you didn’t author, you will find this feature to be very valuable.
any用或注释unknown并使用类型谓词(参见配方 3.5以缩小到特定的错误类型)。
Annotate with any or unknown and use type predicates (see Recipe 3.5 to narrow to specific error types).
如果您以前使用过 Java、C++ 或 C# 等语言,那么您习惯于通过抛出异常并在一系列catch子句中捕获它们来处理错误。可以说有更好的错误处理方法,但这种方法已经存在了很长时间,而且由于历史和影响,它已经进入了JavaScript。1
When you are coming from languages like Java, C++, or C#, you are used to doing your error handling by throwing exceptions and subsequently catching them in a cascade of catch clauses. There are arguably better ways to do error handling, but this one has been around for ages and, given history and influences, has found its way into JavaScript.1
“抛出”错误并“捕获”它们是处理 JavaScript 和 TypeScript 错误的有效方法,但在指定catch子句时有很大区别。当您尝试捕获特定错误类型时,TypeScript 会出错。
“Throwing” errors and “catching” them is a valid way to handle errors in JavaScript and TypeScript, but there is a big difference when it comes to specifying your catch clauses. When you try to catch a specific error type, TypeScript will error.
Example 3-2 uses the popular data-fetching library Axios to show the problem.
try{// something with the popular fetching library Axios, for example}catch(e:AxiosError){// ^^^^^^^^^^ Error 1196: Catch clause variable// type annotation must be 'any' or// 'unknown' if specified.}
try{// something with the popular fetching library Axios, for example}catch(e:AxiosError){// ^^^^^^^^^^ Error 1196: Catch clause variable// type annotation must be 'any' or// 'unknown' if specified.}
造成这种情况的原因如下:
There are a few reasons for this:
在 JavaScript 中,你可以抛出每个表达式。当然,你可以抛出“异常”(或 JavaScript 中称之为错误),但也可以抛出任何其他值:
In JavaScript, you are allowed to throw every expression. Of course, you can throw “exceptions” (or errors, as we call them in JavaScript), but it’s also possible to throw any other value:
throw"What a weird error";// OKthrow404;// OKthrownewError("What a weird error");// OK
throw"What a weird error";// OKthrow404;// OKthrownewError("What a weird error");// OK
由于可以抛出任何有效值,因此捕获的可能值已经比通常的子类型更广泛Error。
Since any valid value can be thrown, the possible values to catch are already broader than your usual subtype of Error.
JavaScript 每个语句只有一个catch子句try。过去曾有人提出过多个catch子句甚至条件表达式,但由于 21 世纪初人们对 JavaScript 缺乏兴趣,这些提议从未实现。
JavaScript has only one catch clause per try statement. In the past there have been proposals for multiple catch clauses and even conditional expressions, but due to the lack of interest in JavaScript in the early 2000s, they never manifested.
相反,您应该使用这个子句catch并执行instanceof检查typeof,正如MDN所建议的那样。
Instead, you should use this one catch clause and do instanceof and typeof checks, as proposed on MDN.
此示例也是在TypeScriptcatch中缩小子句
类型的唯一正确方法:
This example is also the only correct way to narrow types for catch clauses in
TypeScript:
try{myroutine();// There's a couple of errors thrown here}catch(e){if(einstanceofTypeError){// A TypeError}elseif(einstanceofRangeError){// Handle the RangeError}elseif(einstanceofEvalError){// you guessed it: EvalError}elseif(typeofe==="string"){// The error is a string}elseif(axios.isAxiosError(e)){// axios does an error check for us!}else{// everything elselogMyErrors(e);}}
try{myroutine();// There's a couple of errors thrown here}catch(e){if(einstanceofTypeError){// A TypeError}elseif(einstanceofRangeError){// Handle the RangeError}elseif(einstanceofEvalError){// you guessed it: EvalError}elseif(typeofe==="string"){// The error is a string}elseif(axios.isAxiosError(e)){// axios does an error check for us!}else{// everything elselogMyErrors(e);}}
catch由于所有可能的值都可以被抛出,并且每个语句只有一个子句try来处理它们,因此类型范围e非常广泛。
Since all possible values can be thrown, and we only have one catch clause per try statement to handle them, the type range of e is exceptionally broad.
既然你知道可能发生的每种错误,那么包含所有可能“可抛出”的适当联合类型不是同样有效吗?理论上是的。实际上,没有办法知道异常的类型。
Since you know about every error that can happen, wouldn’t a proper union type with all possible “throwables” work just as well? In theory, yes. In practice, there is no way to tell which types the exception will have.
除了所有用户定义的异常和错误之外,当内存出现问题(如类型不匹配或某个函数未定义)时,系统可能会抛出错误。一个简单的函数调用就可能超出您的调用堆栈,并导致臭名昭著的堆栈溢出。
Next to all your user-defined exceptions and errors, the system might throw errors when something is wrong with the memory when it encountered a type mismatch or one of your functions has been undefined. A simple function call could exceed your call stack and cause the infamous stack overflow.
可能值的广泛集合、单个catch子句以及发生的错误的不确定性仅允许两种类型e:any和unknown。
The broad set of possible values, the single catch clause, and the uncertainty of errors that happen allow only two types for e: any and unknown.
如果您拒绝 ,则所有原因都适用Promise。 TypeScript 唯一允许您指定的是已实现 的类型Promise。拒绝可能是您本人所为,也可能是由于系统错误:
All reasons apply if you reject a Promise. The only thing TypeScript allows you to specify is the type of a fulfilled Promise. A rejection can happen on your behalf or through a system error:
constsomePromise=()=>newPromise((fulfil,reject)=>{if(someConditionIsValid()){fulfil(42);}else{reject("Oh no!");}});somePromise().then((val)=>console.log(val))// val is number.catch((e)=>console.log(e));// can be anything, really;
constsomePromise=()=>newPromise((fulfil,reject)=>{if(someConditionIsValid()){fulfil(42);}else{reject("Oh no!");}});somePromise().then((val)=>console.log(val))// val is number.catch((e)=>console.log(e));// can be anything, really;
Promise如果你在async/ flow 中调用相同方法,情况会变得更加清晰await:
It becomes clearer if you call the same Promise in an async/await flow:
try{constz=awaitsomePromise();// z is number}catch(e){// same thing, e can be anything!}
try{constz=awaitsomePromise();// z is number}catch(e){// same thing, e can be anything!}
如果您想定义自己的错误并相应地进行捕获,您可以编写错误类并执行检查实例,或者创建辅助函数来检查某些属性并通过类型谓词告知正确的类型。Axios 也是一个很好的例子:
If you want to define your own errors and catch accordingly, you can either write error classes and do instance of checks or create helper functions that check for certain properties and tell the correct type via type predicates. Axios is again a good example for that:
functionisAxiosError(payload:any):payloadisAxiosError{returnpayload!==null&&typeofpayload==='object'&&payload.isAxiosError;}
functionisAxiosError(payload:any):payloadisAxiosError{returnpayload!==null&&typeofpayload==='object'&&payload.isAxiosError;}
如果您使用过其他具有类似功能的编程语言,那么 JavaScript 和 TypeScript 中的错误处理可能只是个“假朋友”。请注意这些差异,并相信 TypeScript 团队和类型检查器会为您提供正确的控制流程,以确保您的错误得到有效处理。
Error handling in JavaScript and TypeScript can be a “false friend” if you come from other programming languages with similar features. Be aware of the differences and trust the TypeScript team and type-checker to give you the correct control flow to make sure your errors are handled effectively.
使用可选的 never技术来排除某些属性。
Use the optional never technique to exclude certain properties.
您想要编写一个函数来处理应用程序中选择操作的结果。此选择操作为您提供了可能选项的列表以及选定选项的列表。此函数可以处理仅产生单个值的选择操作的调用以及产生多个值的选择操作的调用。
You want to write a function that handles the result of a select operation in your application. This select operation gives you the list of possible options as well as the list of selected options. This function can deal with calls from a select operation that produces only a single value as well as from a select operation that results in multiple values.
由于您需要适应现有的 API,您的函数应该能够处理这两种情况,并决定函数内的单一和多种情况。
Since you need to adapt to an existing API, your function should be able to handle both and decide for the single and multiple cases within the function.
当然,还有更好的方法来建模 API,我们可以无休止地谈论这个问题。但有时你必须处理一开始就不太好的现有 API。在这种情况下,TypeScript 为你提供了正确输入数据的技术和方法。
Of course there are better ways to model APIs, and we can talk endlessly about that. But sometimes you have to deal with existing APIs that are not that great to begin with. TypeScript gives you techniques and methods to correctly type your data in scenarios like this.
您的模型反映了该 API,因为您可以传递一个value或多个
values:
Your model mirrors that API, as you can pass either a single value or multiple
values:
typeSelectBase={options:string[];};typeSingleSelect=SelectBase&{value:string;};typeMultipleSelect=SelectBase&{values:string[];};typeSelectProperties=SingleSelect|MultipleSelect;functionselectCallback(params:SelectProperties){if("value"inparams){// handle single cases}elseif("values"inparams){// handle multiple cases}}selectCallback({options:["dracula","monokai","vscode"],value:"dracula",});selectCallback({options:["dracula","monokai","vscode"],values:["dracula","vscode"],});
typeSelectBase={options:string[];};typeSingleSelect=SelectBase&{value:string;};typeMultipleSelect=SelectBase&{values:string[];};typeSelectProperties=SingleSelect|MultipleSelect;functionselectCallback(params:SelectProperties){if("value"inparams){// handle single cases}elseif("values"inparams){// handle multiple cases}}selectCallback({options:["dracula","monokai","vscode"],value:"dracula",});selectCallback({options:["dracula","monokai","vscode"],values:["dracula","vscode"],});
这可以按预期工作,但请记住 TypeScript 的结构类型系统功能。定义SingleSelect为类型还允许所有子类型的值,这意味着同时具有属性value和values属性的对象也与兼容SingleSelect。这同样适用于。没有什么可以阻止您将函数与同时包含以下内容的对象一起MultipleSelect使用:selectCallback
This works as intended, but remember the structural type system features of TypeScript. Defining SingleSelect as a type allows also for values of all subtypes, which means that objects that have both the value property and the values property are also compatible to SingleSelect. The same goes for MultipleSelect. Nothing keeps you from using the selectCallback function with an object that contains both:
selectCallback({options:["dracula","monokai","vscode"],values:["dracula","vscode"],value:"dracula",});// still works! Which one to choose?
selectCallback({options:["dracula","monokai","vscode"],values:["dracula","vscode"],value:"dracula",});// still works! Which one to choose?
您在此处传递的值是有效的,但它在您的应用程序中没有意义。您无法确定这是一个多选操作还是单选操作。
The value you pass here is valid, but it doesn’t make sense in your application. You couldn’t decide whether this is a multiple select operation or a single select operation.
在这种情况下,我们需要再次将两组价值观分开,以便我们的模型变得更加清晰。我们可以通过使用可选never技术来实现这一点。2它涉及获取联合的每个分支所独有的属性,并将它们作为类型的可选属性添加never到其他分支:
In cases like this we again need to separate the two sets of values just enough so our model becomes clearer. We can do this by using the optional never technique.2 It involves taking the properties that are exclusive to each branch of a union and adding them as optional properties of type never to the other branches:
typeSelectBase={options:string[];};typeSingleSelect=SelectBase&{value:string;values?:never;};typeMultipleSelect=SelectBase&{value?:never;values:string[];};
typeSelectBase={options:string[];};typeSingleSelect=SelectBase&{value:string;values?:never;};typeMultipleSelect=SelectBase&{value?:never;values:string[];};
你告诉 TypeScript,此属性在此分支中是可选的,并且设置后,它没有兼容的值。这样,所有包含这两个属性的对象都无效SelectProperties:
You tell TypeScript that this property is optional in this branch, and when it’s set, there is no compatible value for it. With that, all objects that contain both properties are invalid to SelectProperties:
selectCallback({options:["dracula","monokai","vscode"],values:["dracula","vscode"],value:"dracula",});// ^ Argument of type '{ options: string[]; values: string[]; value: string; }'// is not assignable to parameter of type 'SelectProperties'.
selectCallback({options:["dracula","monokai","vscode"],values:["dracula","vscode"],value:"dracula",});// ^ Argument of type '{ options: string[]; values: string[]; value: string; }'// is not assignable to parameter of type 'SelectProperties'.
联合类型再次被分开,但不包含属性kind。这对于区分属性只有几个的模型非常有用。如果您的模型具有太多不同的属性,并且您可以添加属性kind,请使用区分联合类型,如方案 3.2所示。
The union types are separated again, without the inclusion of a kind property. This works great for models where the discriminating properties are just a few. If your model has too many distinct properties, and you can afford to add a kind property, use discriminated union types as shown in Recipe 3.2.
使用类型断言通过关键字缩小到更小的集合as,表示不安全的操作。
Use type assertions to narrow to a smaller set using the as keyword, indicating an unsafe operation.
想象一下掷骰子并产生一个介于 1 和 6 之间的数字。JavaScript 函数只有一行,使用了 Math 库。您想使用一个狭窄的类型,即表示结果的六个文字数字类型的联合。但是,您的操作产生了一个number,并且number对于您的结果来说类型太宽了:
Think of rolling a die and producing a number between one and six. The JavaScript function is one line, using the Math library. You want to work with a narrowed type, a union of six literal number types indicating the results. However, your operation produces a number, and number is a type too wide for your results:
typeDice=1|2|3|4|5|6;functionrollDice():Dice{letnum=Math.floor(Math.random()*6)+1;returnnum;//^ Type 'number' is not assignable to type 'Dice'.(2322)}
typeDice=1|2|3|4|5|6;functionrollDice():Dice{letnum=Math.floor(Math.random()*6)+1;returnnum;//^ Type 'number' is not assignable to type 'Dice'.(2322)}
由于number允许的值比更多Dice,TypeScript 不允许您仅通过注释函数签名来缩小类型。这仅在类型更宽时才有效,即超类型:
Since number allows for more values than Dice, TypeScript won’t allow you to narrow the type just by annotating the function signature. This works only if the type is wider, a supertype:
// All dice are numbersfunctionasNumber(dice:Dice):number{returndice;}
// All dice are numbersfunctionasNumber(dice:Dice):number{returndice;}
相反,就像方案 3.5中的类型谓词一样,我们可以通过断言类型比预期的要窄来告诉 TypeScript 我们知道得更多:
Instead, just like with type predicates from Recipe 3.5, we can tell TypeScript that we know better, by asserting that the type is narrower than expected:
typeDice=1|2|3|4|5|6;functionrollDice():Dice{letnum=Math.floor(Math.random()*6)+1;returnnumasDice;}
typeDice=1|2|3|4|5|6;functionrollDice():Dice{letnum=Math.floor(Math.random()*6)+1;returnnumasDice;}
就像类型谓词一样,类型断言仅在假定类型的超类型和子类型中起作用。我们可以将值设置为更宽的超类型,也可以将其更改为更窄的子类型。TypeScript 不允许我们切换集合:
Just like type predicates, type assertions work only within the supertypes and subtypes of an assumed type. We can either set the value to a wider supertype or change it to a narrower subtype. TypeScript won’t allow us to switch sets:
functionasString(num:number):string{returnnumasstring;// ^- Conversion of type 'number' to type 'string' may// be a mistake because neither type sufficiently// overlaps with the other.// If this was intentional, convert the expression to 'unknown' first.}
functionasString(num:number):string{returnnumasstring;// ^- Conversion of type 'number' to type 'string' may// be a mistake because neither type sufficiently// overlaps with the other.// If this was intentional, convert the expression to 'unknown' first.}
使用该as Dice语法非常方便。它表示我们作为开发人员负责的类型更改。这意味着如果出现错误,我们可以轻松扫描代码中的as关键字并找到可能的罪魁祸首。
Using the as Dice syntax is quite handy. It indicates a type change that we as developers are responsible for. This means that if something turns out wrong, we can easily scan our code for the as keyword and find possible culprits.
在日常语言中,人们倾向于将类型断言称为类型转换。这可能来自于它与 C、Java 等语言中实际的、显式的类型转换的相似性。然而,类型断言与类型转换非常不同。类型转换不仅会改变兼容值的集合,还会改变内存布局甚至值本身。将浮点数转换为整数将截断尾数。另一方面,TypeScript 中的类型断言只会改变兼容值的集合。值保持不变。之所以称为类型断言,是因为你断言类型要么更窄要么更宽,从而为类型系统提供更多提示。所以如果你正在讨论改变类型,请将它们称为断言,而不是转换。
In everyday language, people tend to call type assertions type casts. This arguably comes from similarity to actual, explicit type casts in C, Java, and the like. However, a type assertion is very different from a type cast. A type cast not only changes the set of compatible values but also changes the memory layout and even the values themselves. Casting a floating point number to an integer will cut off the mantissa. A type assertion in TypeScript, on the other hand, changes only the set of compatible values. The value stays the same. It’s called a type assertion because you assert that the type is something either narrower or wider, giving more hints to the type system. So if you are in a discussion on changing types, call them assertions, not casts.
断言也经常用于组合对象的属性。例如,你知道形状将是,Person但你需要先设置属性:
Assertions are also often used when you assemble the properties of an object. You know that the shape is going to be of, for example, Person, but you need to set the properties first:
typePerson={name:string;age:number;};functioncreateDemoPerson(name:string){constperson={}asPerson;person.name=name;person.age=Math.floor(Math.random()*95);returnperson;}
typePerson={name:string;age:number;};functioncreateDemoPerson(name:string){constperson={}asPerson;person.name=name;person.age=Math.floor(Math.random()*95);returnperson;}
类型断言告诉 TypeScript 空对象应该Person位于末尾。随后,TypeScript 允许您设置属性。这也是一个不安全的操作,因为您可能会忘记设置属性,而 TypeScript 不会抱怨。更糟糕的是,Person可能会更改并获取更多属性,而您根本无法得到缺少属性的提示:
A type assertion tells TypeScript that the empty object is supposed to be Person at the end. Subsequently, TypeScript allows you to set properties. It’s also an unsafe operation, because you might forget that you set a property and TypeScript would not complain. Even worse, Person might change and get more properties, and you would get no indication at all that you are missing properties:
typePerson={name:string;age:number;profession:string;};functioncreateDemoPerson(name:string){constperson={}asPerson;person.name=name;person.age=Math.floor(Math.random()*95);// Where's Profession?returnperson;}
typePerson={name:string;age:number;profession:string;};functioncreateDemoPerson(name:string){constperson={}asPerson;person.name=name;person.age=Math.floor(Math.random()*95);// Where's Profession?returnperson;}
在这种情况下,最好选择安全的对象创建。没有什么可以阻止你注释并确保使用赋值设置所有必需的属性:
In situations like this, it’s better to opt for a safe object creation. Nothing keeps you from annotating and making sure that you set all the required properties with the assignment:
typePerson={name:string;age:number;};functioncreateDemoPerson(name:string){constperson:Person={name,age:Math.floor(Math.random()*95),};returnperson;}
typePerson={name:string;age:number;};functioncreateDemoPerson(name:string){constperson:Person={name,age:Math.floor(Math.random()*95),};returnperson;}
虽然类型注释比类型断言更安全,但在某些情况下rollDice没有更好的选择。在其他 TypeScript 场景中,您确实可以选择,但即使您可以注释,也可能希望优先使用类型断言。
While type annotations are safer than type assertions, in situations like rollDice there is no better choice. In other TypeScript scenarios you do have a choice but might want to prefer type assertions, even if you could annotate.
当我们使用fetchAPI 时,例如从后端获取 JSON 数据,我们可以调用fetch并将结果分配给带注释的类型:
When we use the fetch API, for example, getting JSON data from a backend, we can call fetch and assign the results to an annotated type:
typePerson={name:string;age:number;};constppl:Person[]=awaitfetch("/api/people").then((res)=>res.json());
typePerson={name:string;age:number;};constppl:Person[]=awaitfetch("/api/people").then((res)=>res.json());
res.json()结果是any,并且所有 都any可以通过类型注释更改为任何其他类型。不能保证结果实际上是Person[]。我们可以用不同的方式编写同一行,通过断言结果是
Person[],将范围缩小any到更具体的范围:
res.json() results in any, and everything that is any can be changed to any other type through a type annotation. There is no guarantee that the results are actually Person[]. We can write the same line differently, by asserting that the result is a
Person[], narrowing any to something more specific:
constppl=awaitfetch("/api/people").then((res)=>res.json())asPerson[];
constppl=awaitfetch("/api/people").then((res)=>res.json())asPerson[];
对于类型系统来说,这是同样的事情,但我们可以轻松扫描可能存在问题的情况。如果模型发生"/api/people"变化怎么办?如果我们只是寻找注释,就更难发现错误。这里的断言表明存在不安全的操作。
For the type system, this is the same thing, but we can easily scan situations where there might be problems. What if the model in "/api/people" changes? It’s harder to spot errors if we are just looking for annotations. An assertion here is an indicator of an unsafe operation.
真正有帮助的是考虑创建一组在应用程序边界内工作的模型。当你依赖外部的东西时,比如 API,或者数字的正确计算,类型断言可以表明你已经越界了。
What really helps is to think of creating a set of models that works within your application boundaries. The moment you rely on something from the outside, like APIs, or the correct calculation of a number, type assertions can indicate that you’ve crossed the boundary.
就像使用类型谓词(参见范例 3.5)一样,类型断言将确定正确类型的责任交到了您的手中。请明智地使用它们。
Just like using type predicates (see Recipe 3.5), type assertions put the responsibility of a correct type in your hands. Use them wisely.
使用索引签名来定义一组开放的密钥但具有定义的值类型。
Use index signatures to define an open set of keys but with defined value types.
Web API 中有一种样式,您可以以 JavaScript 对象的形式获取集合,其中属性名称大致相当于唯一标识符,值具有相同的形状。如果您最关心的是键,那么这种样式就很棒,因为一个简单的Object.keys调用就可以为您提供所有相关的 ID,让您可以快速过滤和索引您要查找的值。
There is a style in web APIs where you get collections in the form of a JavaScript object, where the property name is roughly equivalent to a unique identifier and the values have the same shape. This style is great if you are mostly concerned about keys, as a simple Object.keys call gives you all relevant IDs, allowing you to quickly filter and index the values you are looking for.
让我们考虑一下对您所有网站的性能评估,您可以收集相关的性能指标并按域名对其进行分组:
Let’s think of a performance review across all your websites, where you gather relevant performance metrics and group them by the domain’s name:
consttimings={"fettblog.eu":{ttfb:300,fcp:1000,si:1200,lcp:1500,tti:1100,tbt:10,},"typescript-book.com":{ttfb:400,fcp:1100,si:1100,lcp:2200,tti:1100,tbt:0,},};
consttimings={"fettblog.eu":{ttfb:300,fcp:1000,si:1200,lcp:1500,tti:1100,tbt:10,},"typescript-book.com":{ttfb:400,fcp:1100,si:1100,lcp:2200,tti:1100,tbt:0,},};
如果我们想要找到给定指标所需时间最少的域,我们可以创建一个函数,循环遍历所有键,索引每个指标条目,然后进行比较:
If we want to find the domain with the lowest timing for a given metric, we can create a function where we loop over all keys, index each metrics entry, and compare:
functionfindLowestTiming(collection,metric){letresult={domain:"",value:Number.MAX_VALUE,};for(constdomainincollection){consttiming=collection[domain];if(timing[metric]<result.value){result.domain=domain;result.value=timing[metric];}}returnresult.domain;}
functionfindLowestTiming(collection,metric){letresult={domain:"",value:Number.MAX_VALUE,};for(constdomainincollection){consttiming=collection[domain];if(timing[metric]<result.value){result.domain=domain;result.value=timing[metric];}}returnresult.domain;}
我们是优秀的程序员,我们希望相应地输入函数,以确保我们不会传递任何与我们的指标集合想法不符的数据。在右侧输入指标的值非常简单:
As we are good programmers, we want to type our function accordingly so that we make sure we don’t pass any data that doesn’t match our idea of a metric collection. Typing the value for the metrics on the righthand side is pretty straightforward:
typeMetrics={// Time to first bytettfb:number;// First contentful paintfcp:number;// Speed Indexsi:number;// Largest contentful paintlcp:number;// Time to interactivetti:number;// Total blocking timetbt:number;};
typeMetrics={// Time to first bytettfb:number;// First contentful paintfcp:number;// Speed Indexsi:number;// Largest contentful paintlcp:number;// Time to interactivetti:number;// Total blocking timetbt:number;};
定义具有尚未定义的一组键的形状比较棘手,但 TypeScript 有一个工具可以解决此问题:索引签名。我们可以告诉 TypeScript,我们不知道有哪些属性名称,但我们知道它们将是类型,string并且它们将指向Metrics:
Defining a shape that has a yet-to-be-defined set of keys is trickier, but TypeScript has a tool for that: index signatures. We can tell TypeScript that we don’t know which property names there are, but we know they will be of type string and they will point to Metrics:
typeMetricCollection={[domain:string]:Timings;};
typeMetricCollection={[domain:string]:Timings;};
这就是我们需要输入的全部内容findLowestTiming。我们collection用注释MetricCollection并确保我们只传递Metrics第二个参数的键:
And that’s all we need to type findLowestTiming. We annotate collection with MetricCollection and make sure we only pass keys of Metrics for the second parameter:
functionfindLowestTiming(collection:MetricCollection,key:keyofMetrics):string{letresult={domain:"",value:Number.MAX_VALUE,};for(constdomainincollection){consttiming=collection[domain];if(timing[key]<result.value){result.domain=domain;result.value=timing[key];}}returnresult.domain;}
functionfindLowestTiming(collection:MetricCollection,key:keyofMetrics):string{letresult={domain:"",value:Number.MAX_VALUE,};for(constdomainincollection){consttiming=collection[domain];if(timing[key]<result.value){result.domain=domain;result.value=timing[key];}}returnresult.domain;}
这很棒,但也有一些注意事项。TypeScript 允许您读取任何字符串的属性,但它不会检查该属性是否实际可用,因此请注意:
This is great, but there are some caveats. TypeScript allows you to read properties of any string, but it does not do any checks if the property is actually available, so be aware:
constemptySet:MetricCollection={};lettiming=emptySet["typescript-cookbook.com"].fcp*2;// No type errors!
constemptySet:MetricCollection={};lettiming=emptySet["typescript-cookbook.com"].fcp*2;// No type errors!
将索引签名类型更改为Metrics或undefined是更现实的表示。它表示您可以使用所有可能的字符串进行索引,但可能没有值;这会导致更多的安全措施,但最终是正确的选择:
Changing your index signature type to be either Metrics or undefined is a more realistic representation. It says you can index with all possible strings, but there might be no value; this results in a couple more safeguards but is ultimately the right choice:
typeMetricCollection={[domain:string]:Metrics|undefined;};functionfindLowestTiming(collection:MetricCollection,key:keyofMetrics):string{letresult={domain:"",value:Number.MAX_VALUE,};for(constdomainincollection){consttiming=collection[domain];// Metrics | undefined// extra check for undefined valuesif(timing&&timing[key]<result.value){result.domain=domain;result.value=timing[key];}}returnresult.domain;}constemptySet:MetricCollection={};// access with optional chaining and nullish coalescinglettiming=(emptySet["typescript-cookbook.com"]?.fcp??0)*2;
typeMetricCollection={[domain:string]:Metrics|undefined;};functionfindLowestTiming(collection:MetricCollection,key:keyofMetrics):string{letresult={domain:"",value:Number.MAX_VALUE,};for(constdomainincollection){consttiming=collection[domain];// Metrics | undefined// extra check for undefined valuesif(timing&&timing[key]<result.value){result.domain=domain;result.value=timing[key];}}returnresult.domain;}constemptySet:MetricCollection={};// access with optional chaining and nullish coalescinglettiming=(emptySet["typescript-cookbook.com"]?.fcp??0)*2;
值要么是要么Metrics不是,undefined这不完全像缺失属性,但对于此用例来说,它已经足够接近并且足够好了。您可以在第 3.11 节中阅读有关缺失属性和未定义值之间的细微差别。要将属性键设置为可选,您可以告诉 TypeScript 这domain不是整个集合,而是具有所谓映射类型string的子集:string
The value being either Metrics or undefined is not exactly like a missing property, but it’s close enough and good enough for this use case. You can read about the nuance between missing properties and undefined values in Recipe 3.11. To set the property keys as optional, you tell TypeScript that domain is not the entire set of string but a subset of string with a so-called mapped type:
typeMetricCollection={[domaininstring]?:Metrics;};
typeMetricCollection={[domaininstring]?:Metrics;};
您可以为所有有效属性键定义索引签名:string、number或symbol,并且对于映射类型,还可以为这些属性键的子集定义索引签名。例如,您可以定义一个类型来仅索引骰子的有效面:
You can define index signatures for everything that is a valid property key: string, number, or symbol, and with mapped types also everything that is a subset of those. For example, you can define a type to index only valid faces of a die:
typeThrows={[xin1|2|3|4|5|6]:number;};
typeThrows={[xin1|2|3|4|5|6]:number;};
您还可以向类型添加属性。以这个ElementCollection为例,它允许您通过数字索引项目,但还具有其他属性get和filter函数以及属性length:
You can also add properties to your type. Take this ElementCollection, for example, which allows you to index items via a number but also has additional properties for get and filter functions as well as a length property:
typeElementCollection={[y:number]:HTMLElement|undefined;get(index:number):HTMLElement|undefined;length:number;filter(callback:(element:HTMLElement)=>boolean):ElementCollection;};
typeElementCollection={[y:number]:HTMLElement|undefined;get(index:number):HTMLElement|undefined;length:number;filter(callback:(element:HTMLElement)=>boolean):ElementCollection;};
如果将索引签名与其他属性组合,则需要确保索引签名的更广泛集合包含特定属性的类型。在上例中,数字索引
签名与其他属性的字符串键之间没有重叠,但如果您定义一个映射到的字符串索引签名string,并希望在其旁边有一个count类型的属性,则 TypeScript 将出错:number
If you combine your index signatures with other properties, you need to make sure that the broader set of your index signature includes the types from the specific properties. In the previous example there is no overlap between the number index
signature and the string keys of your other properties, but if you define an index signature of strings that maps to string and want to have a count property of type number next to it, TypeScript will error:
typeStringDictionary={[index:string]:string;count:number;// Error: Property 'count' of type 'number' is not assignable// to 'string' index type 'string'.(2411)};
typeStringDictionary={[index:string]:string;count:number;// Error: Property 'count' of type 'number' is not assignable// to 'string' index type 'string'.(2411)};
这是有道理的:如果所有字符串键都指向一个字符串,为什么还要count指向其他东西呢?这会产生歧义,TypeScript 不允许这样做。您必须扩大索引签名的类型,以确保较小的集合是较大集合的一部分:
And it makes sense: if all string keys point to a string, why would count point to something else? There’s ambiguity, and TypeScript won’t allow this. You would have to widen the type of your index signature to make sure that the smaller set is part of the bigger set:
typeStringOrNumberDictionary={[index:string]:string|number;count:number;// works};
typeStringOrNumberDictionary={[index:string]:string|number;count:number;// works};
现在count对索引签名的类型和属性值的类型进行子集化。
Now count subsets both the type from the index signature and the type of the property’s value.
索引签名和映射类型是功能强大的工具,可让您使用 Web API 以及允许灵活访问元素的数据结构。我们在 JavaScript 中了解和喜爱的东西现在已在 TypeScript 中安全地输入。
Index signatures and mapped types are powerful tools that allow you to work with web APIs as well as data structures that allow for flexible access to elements. Something that we know and love from JavaScript is now securely typed in TypeScript.
exactOptionalPropertyTypes在tsconfig中激活以启用对可选属性的更严格处理。
Activate exactOptionalPropertyTypes in tsconfig to enable stricter handling of optional properties.
我们的软件有用户设置,我们可以在其中定义用户的语言及其首选的颜色覆盖。这是一个附加主题,这意味着基本颜色已在样式中设置"default"。这意味着用户设置theme是可选的:要么可用,要么不可用。我们使用 TypeScript 的可选属性
来实现这一点:
Our software has user settings where we can define the user’s language and their preferred color overrides. It’s an additional theme, which means that the basic colors are already set in a "default" style. This means that the user setting for theme is optional: either it is available or it isn’t. We use TypeScript’s optional properties
for that:
typeSettings={language:"en"|"de"|"fr";theme?:"dracula"|"monokai"|"github";};
typeSettings={language:"en"|"de"|"fr";theme?:"dracula"|"monokai"|"github";};
使用strictNullChecksactive,访问theme代码中的某个位置可以扩大可能值的数量。您不仅可以覆盖三个主题,还可以覆盖undefined:
With strictNullChecks active, accessing theme somewhere in your code widens the number of possible values. You have not only the three theme overrides but also the possibility of undefined:
functionapplySettings(settings:Settings){// theme is "dracula" | "monokai" | "github" | undefinedconsttheme=settings.theme;}
functionapplySettings(settings:Settings){// theme is "dracula" | "monokai" | "github" | undefinedconsttheme=settings.theme;}
这是非常好的行为,因为您确实希望确保设置了此属性;否则,可能会导致运行时错误。TypeScriptundefined向可选属性的可能值列表中添加内容是好的,但它并不完全反映 JavaScript 的行为。可选属性意味着对象中缺少此键,这很微妙但很重要。例如,在属性检查中会返回缺少的键false:
This is great behavior, as you really want to make sure that this property is set; otherwise, it could result in runtime errors. TypeScript adding undefined to the list of possible values of optional properties is good, but it doesn’t entirely mirror the behavior of JavaScript. Optional properties means that this key is missing from the object, which is subtle but important. For example, a missing key would return false in property checks:
functiongetTheme(settings:Settings){if('theme'insettings){// only true if the property is set!returnsettings.theme;}return'default';}constsettings:Settings={language:"de",};constsettingsUndefinedTheme:Settings={language:"de",theme:undefined,};console.log(getTheme(settings))// "default"console.log(getTheme(settingsUndefinedTheme))// undefined
functiongetTheme(settings:Settings){if('theme'insettings){// only true if the property is set!returnsettings.theme;}return'default';}constsettings:Settings={language:"de",};constsettingsUndefinedTheme:Settings={language:"de",theme:undefined,};console.log(getTheme(settings))// "default"console.log(getTheme(settingsUndefinedTheme))// undefined
在这里,尽管两个设置对象看起来很相似,但我们得到了完全不同的结果。更糟糕的是,主题undefined是我们认为无效的值。不过,TypeScript 不会对我们撒谎,因为它完全知道检查in只会告诉我们属性是否可用。可能的返回值还getTheme包括:undefined
Here, we get entirely different results even though the two settings objects seem similar. What’s worse is that an undefined theme is a value we don’t consider valid. TypeScript doesn’t lie to us, though, as it’s fully aware that an in check only tells us if the property is available. The possible return values of getTheme include undefined
as well:
typeFn=typeofgetTheme;// type Fn = (settings: Settings)// => "dracula" | "monokai" | "github" | "default" | undefined
typeFn=typeofgetTheme;// type Fn = (settings: Settings)// => "dracula" | "monokai" | "github" | "default" | undefined
并且可能有更好的检查方法来查看这里的值是否正确。使用空值合并后,上述代码变为:
And there are arguably better checks to see if the correct values are here. With nullish coalescing the preceding code becomes:
functiongetTheme(settings:Settings){returnsettings.theme??"default";}typeFn=typeofgetTheme;// type Fn = (settings: Settings)// => "dracula" | "monokai" | "github" | "default"
functiongetTheme(settings:Settings){returnsettings.theme??"default";}typeFn=typeofgetTheme;// type Fn = (settings: Settings)// => "dracula" | "monokai" | "github" | "default"
尽管如此,in检查仍然有效并被开发人员使用,而 TypeScript 解释可选属性的方式可能会导致歧义。undefined从可选属性读取是正确的,但将可选属性设置为undefined则不正确。通过打开exactOptionalPropertyTypes,TypeScript 会更改此行为:
Still, in checks are valid and used by developers, and the way TypeScript interprets optional properties can cause ambiguity. Reading undefined from an optional property is correct, but setting optional properties to undefined isn’t. By switching on exactOptionalPropertyTypes, TypeScript changes this behavior:
// exactOptionalPropertyTypes is trueconstsettingsUndefinedTheme:Settings={language:"de",theme:undefined,};// Error: Type '{ language: "de"; theme: undefined; }' is// not assignable to type 'Settings' with 'exactOptionalPropertyTypes: true'.// Consider adding 'undefined' to the types of the target's properties.// Types of property 'theme' are incompatible.// Type 'undefined' is not assignable to type// '"dracula" | "monokai" | "github"'.(2375)
// exactOptionalPropertyTypes is trueconstsettingsUndefinedTheme:Settings={language:"de",theme:undefined,};// Error: Type '{ language: "de"; theme: undefined; }' is// not assignable to type 'Settings' with 'exactOptionalPropertyTypes: true'.// Consider adding 'undefined' to the types of the target's properties.// Types of property 'theme' are incompatible.// Type 'undefined' is not assignable to type// '"dracula" | "monokai" | "github"'.(2375)
exactOptionalPropertyTypes使 TypeScript 的行为与 JavaScript 更加一致。strict但是,此标志不在模式内,因此如果遇到此类问题,您需要自行设置。
exactOptionalPropertyTypes aligns TypeScript’s behavior even more to JavaScript. This flag is not within strict mode, however, so you need to set it yourself if you encounter problems like this.
谨慎使用它们,优先使用const枚举,了解它们的注意事项,也许选择联合类型。
Use them sparingly, prefer const enums, know their caveats, and maybe choose union types instead.
TypeScript 中的枚举允许开发人员定义一组命名常量,这使得记录意图或创建一组不同的案例变得更加容易。
Enums in TypeScript allow a developer to define a set of named constants, which makes it easier to document intent or create a set of distinct cases.
它们使用enum关键字定义:
They’re defined using the enum keyword:
enumDirection{Up,Down,Left,Right,};
enumDirection{Up,Down,Left,Right,};
与类一样,它们有助于值和类型命名空间,这意味着您可以Direction在注释类型时或在 JavaScript 代码中将其用作值:
Like classes, they contribute to the value and type namespaces, which means you can use Direction when annotating types or in your JavaScript code as values:
// used as typefunctionmove(direction:Direction){// ...}// used as valuemove(Direction.Up);
// used as typefunctionmove(direction:Direction){// ...}// used as valuemove(Direction.Up);
它们是 JavaScript 的语法扩展,这意味着它们不仅可以在类型系统级别工作,还可以发出 JavaScript 代码:
They are a syntactic extension to JavaScript, which means they not only work on a type system level but also emit JavaScript code:
varDirection;(function(Direction){Direction[Direction["Up"]=0]="Up";Direction[Direction["Down"]=1]="Down";Direction[Direction["Left"]=2]="Left";Direction[Direction["Right"]=3]="Right";})(Direction||(Direction={}));
varDirection;(function(Direction){Direction[Direction["Up"]=0]="Up";Direction[Direction["Down"]=1]="Down";Direction[Direction["Left"]=2]="Left";Direction[Direction["Right"]=3]="Right";})(Direction||(Direction={}));
当你将枚举定义为时const enum,TypeScript 会尝试用实际值替换用法,摆脱发出的代码:
When you define your enum as a const enum, TypeScript tries to substitute the usage with the actual values, getting rid of the emitted code:
constenumDirection{Up,Down,Left,Right,};// When having a const enum, TypeScript// transpiles move(Direction.Up) to this:move(0/* Direction.Up */);
constenumDirection{Up,Down,Left,Right,};// When having a const enum, TypeScript// transpiles move(Direction.Up) to this:move(0/* Direction.Up */);
TypeScript 同时支持字符串和数字枚举,并且两种变体的行为 非常不同。
TypeScript supports both string and numeric enums, and both variants behave very differently.
TypeScript 枚举默认是数字的,这意味着该枚举的每个变体都分配有一个从 0 开始的数值。枚举变体的起点和实际值可以是默认值或用户定义的:
TypeScript enums are by default numeric, which means that every variant of that enum has a numeric value assigned, starting at 0. The starting point and actual values of enum variants can be a default or user defined:
// DefaultenumDirection{Up,// 0Down,// 1Left,// 2Right,// 3};enumDirection{Up=1,// 1Down,// 2Left,// 3Right=5,// 5};
// DefaultenumDirection{Up,// 0Down,// 1Left,// 2Right,// 3};enumDirection{Up=1,// 1Down,// 2Left,// 3Right=5,// 5};
在某种程度上,数字枚举定义与数字联合类型相同的集合:
In a way, numeric enums define the same set as a union type of numbers:
typeDirection=0|1|2|3;
typeDirection=0|1|2|3;
但也有显著的区别。联合类型的数字只允许严格定义的一组值,而数字枚举允许分配每个值:
But there are significant differences. Where a union type of numbers allows only a strictly defined set of values, a numeric enum allows for every value to be assigned:
functionmove(direction:Direction){/* ... */}move(30);// This is ok!
functionmove(direction:Direction){/* ... */}move(30);// This is ok!
原因是存在使用数字枚举实现标志的用例:
The reason is that there is a use case of implementing flags with numeric enums:
// Possible traits of a person, can be multipleenumTraits{None,// 0000Friendly=1,// 0001 or 1 << 0Mean=1<<1,// 0010Funny=1<<2,// 0100Boring=1<<3,// 1000}// (0010 | 0100) === 0110letaPersonsTraits=Traits.Mean|Traits.Funny;if((aPersonsTraits&Traits.Mean)===Traits.Mean){// Person is mean, amongst other things}
// Possible traits of a person, can be multipleenumTraits{None,// 0000Friendly=1,// 0001 or 1 << 0Mean=1<<1,// 0010Funny=1<<2,// 0100Boring=1<<3,// 1000}// (0010 | 0100) === 0110letaPersonsTraits=Traits.Mean|Traits.Funny;if((aPersonsTraits&Traits.Mean)===Traits.Mean){// Person is mean, amongst other things}
枚举为这种场景提供了语法糖。为了让编译器更容易看到哪些值是允许的,TypeScript 将数字枚举的兼容值扩展为整个集合number。
Enums provide syntactic sugar for this scenario. To make it easier for the compiler to see which values are allowed, TypeScript expands compatible values for numeric enums to the entire set of number.
枚举变量也可以用字符串而不是数字初始化,从而有效地创建字符串枚举。如果您选择编写字符串枚举,则必须定义每个变量,因为字符串不能递增:
Enum variants can also be initialized with strings instead of numbers, effectively creating a string enum. If you choose to write a string enum, you have to define each variant, as strings can’t be incremented:
enumStatus{Admin="Admin",User="User",Moderator="Moderator",};
enumStatus{Admin="Admin",User="User",Moderator="Moderator",};
字符串枚举比数字枚举更具限制性。它们只允许您传递枚举的实际变体,而不是整个字符串集。但是,它们不允许您传递字符串等效项:
String enums are more restrictive than numeric enums. They only allow you to pass actual variants of the enum rather than the entire set of strings. However, they don’t allow you to pass the string equivalent:
functioncloseThread(threadId:number,status:Status):{// ...}closeThread(10,"Admin");// ^-- Argument of type '"Admin"' is not assignable to// parameter of type 'Status'closeThread(10,Status.Admin);// This works
functioncloseThread(threadId:number,status:Status):{// ...}closeThread(10,"Admin");// ^-- Argument of type '"Admin"' is not assignable to// parameter of type 'Status'closeThread(10,Status.Admin);// This works
与 TypeScript 中的其他类型不同,字符串枚举是名义类型。这也意味着具有相同值集的两个枚举彼此不兼容:
Unlike every other type in TypeScript, string enums are nominal types. This also means two enums with the same set of values are not compatible with each other:
enumRoles{Admin="Admin",User="User",Moderator="Moderator",};closeThread(10,Roles.Admin);// ^-- Argument of type 'Roles.Admin' is not// assignable to parameter of type 'Status'
enumRoles{Admin="Admin",User="User",Moderator="Moderator",};closeThread(10,Roles.Admin);// ^-- Argument of type 'Roles.Admin' is not// assignable to parameter of type 'Status'
这可能会造成混乱和沮丧,尤其是当值来自不了解您的枚举但具有正确字符串值的另一个来源时。
This can be a source of confusion and frustration, especially when values come from another source that doesn’t have knowledge of your enums but does have the correct string values.
明智地使用枚举并了解其注意事项。枚举非常适合功能标志和一组命名常量,您有意让人们使用数据结构而不仅仅是值。
Use enums wisely and know their caveats. Enums are great for feature flags and a set of named constants where you intentionally want people to use the data structure instead of just values.
自 TypeScript 5.0 以来,数字枚举的解释变得更加严格;现在它们的行为与字符串枚举一样,作为名义类型,并且不包含整个数字集作为值。您仍然可能会发现依赖于 5.0 之前的数字枚举的独特功能的代码库,因此请注意!
Since TypeScript 5.0, the interpretation of number enums has become much stricter; now they behave, like string enums, as nominal types and don’t include the entire set of numbers as values. You still might find codebases that rely on the unique features of pre-5.0 number enums, so be aware!
此外,尽可能优先使用const枚举,因为非const枚举会增加代码库的大小,而这些代码库可能是多余的。我见过一些项目在非枚举中使用了超过两千个标志const,这会导致巨大的工具开销、编译时开销,以及随后的运行时开销。
Also try to prefer const enums wherever possible, as non-const enums can add size to your codebase that might be redundant. I have seen projects with more than two thousand flags in a non-const enum, resulting in huge tooling overhead, compile time overhead, and subsequently, runtime overhead.
或者根本不要使用它们。简单联合类型的工作原理类似,并且与类型系统的其余部分更加一致:
Or, don’t use them at all. A simple union type works similarly and is much more aligned with the rest of the type system:
typeStatus="Admin"|"User"|"Moderator";functioncloseThread(threadId:number,status:Status){// ...}closeThread(10,"Admin");// All good
typeStatus="Admin"|"User"|"Moderator";functioncloseThread(threadId:number,status:Status){// ...}closeThread(10,"Admin");// All good
你可以从枚举中获得所有好处,例如适当的工具和类型安全,而无需进行额外的操作并冒着输出你不想要的代码的风险。你还需要更清楚地了解需要传递什么以及从哪里获取值。
You get all the benefits from enums such as proper tooling and type safety without going the extra round and risking outputting code that you don’t want. It also becomes clearer what you need to pass and where to get the value from.
如果您想使用对象和命名标识符以枚举样式编写代码,则const具有Values辅助类型的对象可能只会为您提供所需的行为,并且更接近 JavaScript。同样的技术也适用于字符串联合:
If you want to write your code enum-style, with an object and a named identifier, a const object with a Values helper type might just give you the desired behavior and is much closer to JavaScript. The same technique is also applicable to string unions:
constDirection={Up:0,Down:1,Left:2,Right:3,}asconst;// Get to the const values of DirectiontypeDirection=(typeofDirection)[keyoftypeofDirection];// (typeof Direction)[keyof typeof Direction] yields 0 | 1 | 2 | 3functionmove(direction:Direction){// ...}move(30);// This breaks!move(0);//This works!move(Direction.Left);// This also works!
constDirection={Up:0,Down:1,Left:2,Right:3,}asconst;// Get to the const values of DirectiontypeDirection=(typeofDirection)[keyoftypeofDirection];// (typeof Direction)[keyof typeof Direction] yields 0 | 1 | 2 | 3functionmove(direction:Direction){// ...}move(30);// This breaks!move(0);//This works!move(Direction.Left);// This also works!
这句话特别有趣:
This line is particularly interesting:
// = 0 | 1 | 2 | 3typeDirection=(typeofDirection)[keyoftypeofDirection];
// = 0 | 1 | 2 | 3typeDirection=(typeofDirection)[keyoftypeofDirection];
发生了一些不太常见的事:
A few things happen that are not that usual:
我们声明一个与值同名的类型。这是可能的,因为 TypeScript 具有不同的值和类型命名空间。
We declare a type with the same name as a value. This is possible because TypeScript has distinct value and type namespaces.
使用typeof运算符,我们从中获取类型Direction。与const 上下文Direction中一样,我们获取文字类型。
Using the typeof operator, we grab the type from Direction. As Direction is in const context, we get the literal type.
我们用它自己的键来索引 的类型Direction,将所有值留在对象的右侧:0、1、2和3。简而言之:数字的联合类型。
We index the type of Direction with its own keys, leaving us all the values on the righthand side of the object: 0, 1, 2, and 3. In short: a union type of numbers.
使用联合类型不会带来任何意外:
Using union types leaves no surprises:
您知道最终输出的是什么样的代码。
You know what code you end up with within the output.
你不会因为有人决定从字符串枚举转到数字枚举而改变行为。
You don’t end up with changed behavior because somebody decides to go from a string enum to a numeric enum.
在您需要的地方您就有类型安全。
You have type safety where you need it.
您为您的同事和用户提供与枚举相同的便利。
You give your colleagues and users the same conveniences as provided by enums.
但公平地说,简单的字符串联合类型正好满足您的需要:类型安全、自动完成和可预测的行为。
But to be fair, a simple string union type does just what you need: type safety, autocomplete, and predictable behavior.
使用包装类或创建原始类型与文字对象类型的交集并使用它来区分两个整数。
Use wrapping classes or create an intersection of your primitive type with a literal object type and use this to differentiate two integers.
TypeScript 的类型系统是结构化的。这意味着如果两种类型具有相似的形状,则这种类型的值彼此兼容:
TypeScript’s type system is structural. This means that if two types have a similar shape, values of this type are compatible with each other:
typePerson={name:string;age:number;};typeStudent={name:string;age:number;};functionacceptsPerson(person:Person){// ...}conststudent:Student={name:"Hannah",age:27,};acceptsPerson(student);// all ok
typePerson={name:string;age:number;};typeStudent={name:string;age:number;};functionacceptsPerson(person:Person){// ...}conststudent:Student={name:"Hannah",age:27,};acceptsPerson(student);// all ok
JavaScript 严重依赖对象字面量,而 TypeScript 会尝试推断这些字面量的类型或形状。结构化类型系统在这种情况下非常有意义,因为值可以来自任何地方,并且需要与接口和类型定义兼容。
JavaScript relies heavily on object literals, and TypeScript tries to infer the type or shape of those literals. A structural type system makes a lot of sense in this scenario, as values can come from anywhere and need to be compatible with interface and type definitions.
但是,有些情况下你需要对类型进行更明确的定义。对于对象类型,我们在3.2 节中学习了使用属性的可区分联合,或者在3.8 节中学习了使用“可选”的排他或非排他技术。枚举也是名义上的,正如我们在3.12 节中看到的那样。kindneverstring
However, there are situations where you need to be more definitive with your types. For object types, we learned about techniques like discriminated unions with the kind property in Recipe 3.2, or exclusive or with “optional never" in Recipe 3.8. string enums are also nominal, as we see in Recipe 3.12.
这些测量对于对象类型和枚举来说已经足够好了,但是如果您有两个使用与原始类型相同的值集的独立类型,它们就无法解决问题。如果您的八位数帐号和余额都指向该类型,number而您却将它们混淆了,该怎么办?您的资产负债表上出现八位数的数字是一个不错的惊喜,但这可能并不正确。
Those measurements are good enough for object types and enums, but they don’t solve the problem if you have two independent types that use the same set of values as primitive types. What if your eight-digit account number and your balance all point to the number type and you mix them up? Getting an eight-figure number on your balance sheet is a nice surprise, but it’s likely not correct.
或者您可能需要验证用户输入的字符串,并希望确保在程序中只携带经过验证的用户输入,而不是回退到原始的、可能不安全的字符串。
Or perhaps you need to validate user input strings and want to make sure that you carry around only the validated user input in your program, not falling back to the original, probably unsafe, string.
TypeScript 允许您在类型系统中模拟名义类型以获得更高的安全性。诀窍还在于将具有不同属性的可能值集分开,以确保相同的值不会落入同一集合。
TypeScript allows you to mimic nominal types within the type system to get more security. The trick is also to separate the sets of possible values with distinct properties just enough to ensure the same values don’t fall into the same set.
实现此目的的一种方法是包装类。我们不直接使用值,而是将每个值包装在一个类中。使用属性,private kind我们确保它们不会重叠:
One way to achieve this would be wrapping classes. Instead of working with the values directly, we wrap each value in a class. With a private kind property we make sure they don’t overlap:
classBalance{privatekind="balance";value:number;constructor(value:number){this.value=value;}}classAccountNumber{privatekind="account";value:number;constructor(value:number){this.value=value;}}
classBalance{privatekind="balance";value:number;constructor(value:number){this.value=value;}}classAccountNumber{privatekind="account";value:number;constructor(value:number){this.value=value;}}
这里有趣的是,由于我们使用private属性,TypeScript 将区分这两个类。目前,这两个kind属性都是类型string。即使它们具有不同的值,也可以在内部更改它们。但类的工作方式不同。如果存在private或protected成员,则 TypeScript 认为两种类型兼容,如果它们源自同一声明。否则,它们不被视为兼容。
What’s interesting here is that since we use private properties, TypeScript will differentiate between the two classes. Right now, both kind properties are of type string. Even though they feature a different value, they can be changed internally. But classes work differently. If private or protected members are present, TypeScript considers two types compatible if they originate from the same declaration. Otherwise, they aren’t considered compatible.
这使我们能够使用更通用的方法改进此模式。我们不是定义kind成员并将其设置为值,而是_nominal在每个类声明中定义一个类型为的成员void。这恰好将两个类分开,但使我们无法_nominal以任何方式使用。void只允许我们将设置_nominal为
undefined,并且undefined是假的,因此非常无用:
This allows us to refine this pattern with a more general approach. Instead of defining a kind member and setting it to a value, we define a _nominal member in each class declaration that is of type void. This separates both classes just enough but keeps us from using _nominal in just any way. void only allows us to set _nominal to
undefined, and undefined is a falsy, and thus highly useless:
classBalance{private_nominal:void=undefined;value:number;constructor(value:number){this.value=value;}}classAccountNumber{private_nominal:void=undefined;value:number;constructor(value:number){this.value=value;}}constaccount=newAccountNumber(12345678);constbalance=newBalance(10000);functionacceptBalance(balance:Balance){// ...}acceptBalance(balance);// okacceptBalance(account);// ^ Argument of type 'AccountNumber' is not// assignable to parameter of type 'Balance'.// Types have separate declarations of a// private property '_nominal'.(2345)
classBalance{private_nominal:void=undefined;value:number;constructor(value:number){this.value=value;}}classAccountNumber{private_nominal:void=undefined;value:number;constructor(value:number){this.value=value;}}constaccount=newAccountNumber(12345678);constbalance=newBalance(10000);functionacceptBalance(balance:Balance){// ...}acceptBalance(balance);// okacceptBalance(account);// ^ Argument of type 'AccountNumber' is not// assignable to parameter of type 'Balance'.// Types have separate declarations of a// private property '_nominal'.(2345)
现在,我们可以区分具有相同值集的两种类型。这种方法的唯一缺点是,我们包装了原始类型,这意味着每次我们想要使用原始值时,我们都需要将其解包。
We can now differentiate between two types that would have the same set of values. The only downside to this approach is that we wrap the original type, which means that every time we want to work with the original value, we need to unwrap it.
模仿名义类型的另一种方法是将原始类型与
具有属性的品牌对象类型相交kind。这样,我们保留了原始类型的所有操作,但我们需要要求类型断言来告诉 TypeScript 我们想要以不同的方式使用这些类型。
A different way to mimic nominal types is to intersect the primitive type with a
branded object type with a kind property. This way, we retain all the operations from the original type, but we need to require type assertions to tell TypeScript that we want to use those types differently.
正如我们在3.9 节中学到的,如果另一种类型是原始类型的子类型或超类型,我们可以安全地断言它:
As we learned in Recipe 3.9, we can safely assert another type if it is a subtype or supertype of the original:
typeCredits=number&{_kind:"credits"};typeAccountNumber=number&{_kind:"accountNumber"};constaccount=12345678asAccountNumber;letbalance=10000asCredits;constamount=3000asCredits;functionincrease(balance:Credits,amount:Credits):Credits{return(balance+amount)asCredits;}balance=increase(balance,amount);balance=increase(balance,account);// ^ Argument of type 'AccountNumber' is not// assignable to parameter of type 'Credits'.// Type 'AccountNumber' is not assignable to type '{ _kind: "credits"; }'.// Types of property '_kind' are incompatible.// Type '"accountNumber"' is not assignable to type '"credits"'.(2345)
typeCredits=number&{_kind:"credits"};typeAccountNumber=number&{_kind:"accountNumber"};constaccount=12345678asAccountNumber;letbalance=10000asCredits;constamount=3000asCredits;functionincrease(balance:Credits,amount:Credits):Credits{return(balance+amount)asCredits;}balance=increase(balance,amount);balance=increase(balance,account);// ^ Argument of type 'AccountNumber' is not// assignable to parameter of type 'Credits'.// Type 'AccountNumber' is not assignable to type '{ _kind: "credits"; }'.// Types of property '_kind' are incompatible.// Type '"accountNumber"' is not assignable to type '"credits"'.(2345)
balance还要注意,和的加法amount仍然按原计划进行,但会再次产生一个数字。这就是为什么我们需要添加另一个断言:
Also note that the addition of balance and amount still works as originally intended but produces a number again. This is why we need to add another assertion:
constresult=balance+amount;// result is numberconstcredits=(balance+amount)asCredits;// credits is Credits
constresult=balance+amount;// result is numberconstcredits=(balance+amount)asCredits;// credits is Credits
这两种方法各有优缺点,您更喜欢哪一种主要取决于您的场景。这两种方法都是社区根据他们对类型系统行为的理解而开发的解决方法和技术。
Both approaches have their advantages and disadvantages, and whether you prefer one or the other mostly depends on your scenario. Both approaches are workarounds and techniques developed by the community based on their understanding of the type system’s behavior.
GitHub 上的 TypeScript 问题跟踪器上有一些关于为名义类型开放类型系统的讨论,并且这种可能性正在不断研究中。一个想法是使用uniqueSymbols 中的关键字来区分:
There are discussions on the TypeScript issue tracker on GitHub about opening the type system for nomimal types, and the possibility is constantly under investigation. One idea is to use the unique keyword from Symbols to differentiate:
// Hypothetical code, this does not work!typeBalance=uniquenumber;typeAccountNumber=uniquenumber;
// Hypothetical code, this does not work!typeBalance=uniquenumber;typeAccountNumber=uniquenumber;
截至撰写本文时,这个想法以及许多其他想法在未来仍有可能实现。
As time of writing, this idea—and many others—remains a future possibility.
假设您定义了一个用于访问内容管理系统的 API。目前有预定义的内容类型,例如post、page和asset,但开发人员可以定义自己的内容类型。
Let’s say you define an API for access to a content management system. There are predefined content types like post, page, and asset, but developers can define their own.
您可以创建一个retrieve具有单个参数(内容类型)的函数,该函数允许加载条目:
You create a retrieve function with a single parameter, the content type, that allows entries to be loaded:
typeEntry={// tbd.};functionretrieve(contentType:string):Entry[]{// tbd.}
typeEntry={// tbd.};functionretrieve(contentType:string):Entry[]{// tbd.}
效果已经很好了,但你希望给用户提示一下内容类型的默认选项。一种可能性是创建一个辅助类型,将所有预定义的内容类型列为并集的字符串文字string:
This works well enough, but you want to give your users a hint on the default options for content type. A possibility is to create a helper type that lists all predefined content types as string literals in a union with string:
typeContentType="post"|"page"|"asset"|string;functionretrieve(content:ContentType):Entry[]{// tbd}
typeContentType="post"|"page"|"asset"|string;functionretrieve(content:ContentType):Entry[]{// tbd}
这很好地描述了您的情况,但也有一个缺点:post、page和 asset是 的子类型string,因此将它们放在一起会 string有效地将详细信息吞并到更广泛的集合中。
This describes your situation very well but comes with a downside: post, page, and asset are subtypes of string, so putting them in a union with string effectively swallows the detailed information into the broader set.
这意味着您无法通过编辑器获得语句完成提示,如图3-3所示。
This means you don’t get statement completion hints via your editor, as you can see in Figure 3-3.
ContentType至整个 集string,从而吞没了自动完成信息为了保留自动完成信息并保存文字类型,我们需要string与空对象类型相交{}:
To retain autocomplete information and preserve the literal types, we need to intersect string with the empty object type {}:
typeContentType="post"|"page"|"asset"|string&{};
typeContentType="post"|"page"|"asset"|string&{};
此更改的效果更加微妙。它不会将兼容值的数量更改为ContentType,但它会将 TypeScript 设置为一种防止子类型减少并保留文字类型的模式。
The effect of this change is more subtle. It doesn’t alter the number of compatible values to ContentType, but it will set TypeScript into a mode that prevents subtype reduction and preserves the literal types.
您可以在图 3-4中看到效果,其中ContentType没有简化为string,因此所有文字值都可以在文本编辑器中用于语句完成。
You can see the effect in Figure 3-4, where ContentType is not reduced to string, and therefore all literal values are available for statement completion in the text editor.
string与空对象相交保留语句完成提示尽管如此,每个字符串都是有效的ContentType;它只是改变了 API 的开发人员体验并在需要时提供提示。
Still, every string is a valid ContentType; it just changes the developer experience of your API and gives hints where needed.
流行的库如CSSType和React 的 Definitely Typed 类型定义都使用了这种技术。
This technique is used by popular libraries like CSSType or the Definitely Typed type definitions for React.
1例如,Rust 编程语言因其错误处理而受到称赞。
1 For example, the Rust Programming Language has been lauded for its error handling.
2向 Dan Vanderkam 致敬,他在他精彩的Effective TypeScript博客上首次将这项技术称为“可选永不” 。
2 Shout-out to Dan Vanderkam who was first to call this technique “optional never” on his fantastic Effective TypeScript blog.
到目前为止,我们的主要目标是利用 JavaScript 固有的灵活性,并找到一种通过类型系统将其形式化的方法。我们为动态类型语言添加了静态类型,以传达意图、获取工具并在错误 发生之前将其捕获。
Until now, our main goal was to take the inherent flexibility of JavaScript and find a way to formalize it through the type system. We added static types for a dynamically typed language, to communicate intent, get tooling, and catch bugs before they happen.
不过,JavaScript 中的某些部分并不真正关心静态类型。例如,isKeyAvailableInObject函数应该只检查对象中是否有键;它不需要知道具体类型。为了正确地形式化这样的函数,我们可以使用 TypeScript 的结构类型系统,并描述非常广泛的类型(以信息为代价)或非常严格的类型(以
灵活性为代价)。
Some parts in JavaScript don’t really care about static types, though. For example, an isKeyAvailableInObject function should only check if a key is available in an object; it doesn’t need to know about the concrete types. To properly formalize a function like this we can use TypeScript’s structural type system and describe either a very wide type for the price of information or a very strict type for the price of
flexibility.
但我们不想付出任何代价。我们既想要灵活性,又想要信息。TypeScript 中的泛型正是我们需要的灵丹妙药。我们可以描述复杂的关系,并为尚未定义的数据形式化结构。
But we don’t want to pay any price. We want both flexibility and information. Generics in TypeScript are just the silver bullet we need. We can describe complex relationships and formalize structure for data that has not been defined yet.
泛型及其映射类型、类型映射、类型修饰符和辅助类型打开了元类型的大门,我们可以在旧类型的基础上创建新类型,并保持类型之间的关系完整,而新生成的类型会挑战我们原始代码中可能存在的错误。
Generics, along with its gang of mapped types, type maps, type modifiers, and helper types, open the door to metatyping, where we can create new types based on old ones and keep relationships between types intact while the newly generated types challenge our original code for possible bugs.
这是高级 TypeScript 概念的入口。但不要害怕,除非我们定义它们,否则不会有龙。
This is the entrance to advanced TypeScript concepts. But fear not, there shan’t be dragons, unless we define them.
您正在编写一个应用程序,该应用程序将多个语言文件(例如字幕)存储在一个对象中。键是语言代码,值是 URL。您可以通过语言代码选择语言文件来加载它们,语言代码来自某些 API 或用户界面string。为了确保语言代码正确且有效,您添加一个isLanguageAvailable函数来执行in检查并使用类型谓词设置正确的类型:
You are writing an application that stores several language files (for example, subtitles) in an object. The keys are the language codes, and the values are URLs. You load language files by selecting them via a language code, which comes from some API or user interface as string. To make sure the language code is correct and valid, you add an isLanguageAvailable function that does an in check and sets the correct type using a type predicate:
typeLanguages={de:URL;en:URL;pt:URL;es:URL;fr:URL;ja:URL;};functionisLanguageAvailable(collection:Languages,lang:string):langiskeyofLanguages{returnlangincollection;}functionloadLanguage(collection:Languages,lang:string){if(isLanguageAvailable(collection,lang)){// lang is keyof Languagescollection[lang];// access ok!}}
typeLanguages={de:URL;en:URL;pt:URL;es:URL;fr:URL;ja:URL;};functionisLanguageAvailable(collection:Languages,lang:string):langiskeyofLanguages{returnlangincollection;}functionloadLanguage(collection:Languages,lang:string){if(isLanguageAvailable(collection,lang)){// lang is keyof Languagescollection[lang];// access ok!}}
相同的应用程序,不同的场景,完全不同的文件。您将媒体数据加载到 HTML 元素中:音频、视频或canvas元素中某些动画的组合。所有元素都已存在于应用程序中,但您需要根据 API 的输入选择正确的元素。同样,选择以 的形式出现string,然后您编写一个isElementAllowed函数来确保输入实际上是您集合的有效键AllowedElements:
Same application, different scenario, entirely different file. You load media data into an HTML element: either audio, video, or a combination with certain animations in a canvas element. All elements exist in the application already, but you need to select the right one based on input from an API. Again, the selection comes as string, and you write an isElementAllowed function to ensure that the input is actually a valid key of your AllowedElements collection:
typeAllowedElements={video:HTMLVideoElement;audio:HTMLAudioElement;canvas:HTMLCanvasElement;};functionisElementAllowed(collection:AllowedElements,elem:string):elemiskeyofAllowedElements{returnelemincollection;}functionselectElement(collection:AllowedElements,elem:string){if(isElementAllowed(collection,elem)){// elem is keyof AllowedElementscollection[elem];// access ok}}
typeAllowedElements={video:HTMLVideoElement;audio:HTMLAudioElement;canvas:HTMLCanvasElement;};functionisElementAllowed(collection:AllowedElements,elem:string):elemiskeyofAllowedElements{returnelemincollection;}functionselectElement(collection:AllowedElements,elem:string){if(isElementAllowed(collection,elem)){// elem is keyof AllowedElementscollection[elem];// access ok}}
您无需仔细观察就能发现这两种情况非常相似。类型保护函数尤其引人注目。如果我们剥离所有类型信息并对齐名称,它们是相同的:
You don’t need to look too closely to see that both scenarios are very similar. The type guard functions especially catch our eye. If we strip away all the type information and align the names, they are identical:
functionisAvailable(obj,key){returnkeyinobj;}
functionisAvailable(obj,key){returnkeyinobj;}
它们两个的存在都是因为我们得到的类型信息。不是因为输入参数,而是因为类型谓词。在这两种情况下,我们都可以通过断言特定keyof类型来了解有关输入参数的更多信息。
The two of them exist because of the type information we get. Not because of the input parameters, but because of the type predicates. In both scenarios we can tell more about the input parameters by asserting a specific keyof type.
问题在于,集合的两种输入类型完全不同,并且没有重叠。除了空对象,如果我们创建类型,我们不会获得太多有价值的信息keyof。keyof {}实际上是never。
The problem is that both input types for the collection are entirely different and have no overlap. Except for the empty object, for which we don’t get that much valuable information if we create a keyof type. keyof {} is actually never.
但这里有一些我们可以概括的类型信息。我们知道第一个输入参数是一个对象。第二个输入参数是一个属性键。如果此检查的结果为true,我们就知道第一个参数是第二个参数的键。
But there is some type information here that we can generalize. We know the first input parameter is an object. And the second one is a property key. If this check evaluates to true, we know that the first parameter is a key of the second parameter.
为了概括此函数,我们可以将一个泛型类型参数isAvailable添加到名为 的函数中Obj,放在尖括号中。这是一个实际类型的占位符,一旦isAvailable使用,它将被替换。我们可以像使用或一样使用这个泛型类型参数,并且可以添加类型谓词。由于可以替换每个类型,因此需要包含所有可能的属性键—、和:AllowedElementsLanguagesObjkeystringsymbolnumber
To generalize this function, we can add a generic type parameter to isAvailable called Obj, put in angle brackets. This is a placeholder for an actual type that will be substituted once isAvailable is used. We can use this generic type parameter like we would use AllowedElements or Languages and can add a type predicate. Since Obj can be substituted for every type, key needs to include all possible property keys—string, symbol, and number:
functionisAvailable<Obj>(obj:Obj,key:string|number|symbol):keyiskeyofObj{returnkeyinobj;}functionloadLanguage(collection:Languages,lang:string){if(isAvailable(collection,lang)){// lang is keyof Languagescollection[lang];// access ok!}}functionselectElement(collection:AllowedElements,elem:string){if(isAvailable(collection,elem)){// elem is keyof AllowedElementscollection[elem];// access ok}}
functionisAvailable<Obj>(obj:Obj,key:string|number|symbol):keyiskeyofObj{returnkeyinobj;}functionloadLanguage(collection:Languages,lang:string){if(isAvailable(collection,lang)){// lang is keyof Languagescollection[lang];// access ok!}}functionselectElement(collection:AllowedElements,elem:string){if(isAvailable(collection,elem)){// elem is keyof AllowedElementscollection[elem];// access ok}}
就这样:一个函数可以在两种情况下工作,无论我们替换哪种类型Obj。就像 JavaScript 一样!我们仍然可以获得相同的功能,并获得正确的类型信息。索引访问变得安全,而不会牺牲灵活性。
And there you have it: one function that works in both scenarios, no matter which types we substitute Obj for. Just like JavaScript works! We still get the same functionality, and we get the right type information. Index access becomes safe, without sacrificing flexibility.
最好的部分是什么?我们可以isAvailable像使用无类型 JavaScript 等效项一样使用它。这是因为 TypeScript 通过使用推断泛型类型参数的类型。这带来了一些巧妙的副作用。您可以在方案 4.3中阅读更多相关信息。
The best part? We can use isAvailable just like we would use an untyped JavaScript equivalent. This is because TypeScript infers types for generic type parameters through usage. And this comes with some neat side effects. You can read more about that in Recipe 4.3.
当你最终获得实际类型时,请使用泛型类型参数;有关和之间的决策,请参阅2.2 节。anyunknown
Use generic type parameters when you get to the actual type eventually; refer to Recipe 2.2 on the decision between any and unknown.
当我们使用泛型时,它们看起来像是any和 的替代品unknown。以一个identity函数为例——它的唯一工作是返回作为输入参数传递的值
:
When we are using generics, they might seem like a substitute for any and unknown. Take an identity function—its only job is to return the value passed as input
parameter:
functionidentity(value:any):any{returnvalue;}leta=identity("Hello!");letb=identity(false);letc=identity(2);
functionidentity(value:any):any{returnvalue;}leta=identity("Hello!");letb=identity(false);letc=identity(2);
它接受所有类型的值,并且它的返回类型也可以是任何类型。unknown如果我们想安全地访问属性,我们可以使用以下代码编写相同的函数:
It takes values of every type, and the return type of it can also be anything. We can write the same function using unknown if we want to safely access properties:
functionidentity(value:unknown):unknown{returnvalue;}leta=identity("Hello!");letb=identity(false);letc=identity(2);
functionidentity(value:unknown):unknown{returnvalue;}leta=identity("Hello!");letb=identity(false);letc=identity(2);
我们甚至可以混合使用any和unknown,但结果总是相同的:类型信息丢失。返回值的类型就是我们定义的类型。
We can even mix and match any and unknown, but the result is always the same: Type information is lost. The type of the return value is what we define it to be.
现在让我们用泛型代替any或来编写相同的函数unknown。其类型注释表明泛型类型也是返回类型:
Now let’s write the same function with generics instead of any or unknown. Its type annotations say that the generic type is also the return type:
functionidentity<T>(t:T):T{returnt;}
functionidentity<T>(t:T):T{returnt;}
我们可以使用此函数传递任意值并查看 TypeScript 推断出哪种类型:
We can use this function to pass in any value and see which type TypeScript infers:
leta=identity("Hello!");// a is stringletb=identity(2000);// b is numberletc=identity({a:2});// c is { a: number }
leta=identity("Hello!");// a is stringletb=identity(2000);// b is numberletc=identity({a:2});// c is { a: number }
const使用而不是来分配绑定let会产生略有不同的结果:
Assigning to a binding with const instead of let gives slightly different results:
consta=identity("Hello!");// a is "Hello!"constb=identity(2000);// b is 2000constc=identity({a:2});// c is { a: number }
consta=identity("Hello!");// a is "Hello!"constb=identity(2000);// b is 2000constc=identity({a:2});// c is { a: number }
对于原始类型,TypeScript 会用实际类型替换泛型类型参数。我们可以在更高级的场景中充分利用这一点。
For primitive types, TypeScript substitutes the generic type parameter with the actual type. We can make great use of this in more advanced scenarios.
使用 TypeScript 的泛型,还可以注释泛型类型参数:
With TypeScript’s generics, it’s also possible to annotate the generic type parameter:
consta=identity<string>("Hello!");// a is stringconstb=identity<number>(2000);// b is numberconstc=identity<{a:2}>({a:2});// c is { a: 2 }
consta=identity<string>("Hello!");// a is stringconstb=identity<number>(2000);// b is numberconstc=identity<{a:2}>({a:2});// c is { a: 2 }
如果此行为让你想起了3.4 节中描述的注释和推断,那你完全正确。它非常相似,但函数中有泛型类型参数。
If this behavior reminds you of annotation and inference described in Recipe 3.4, you are absolutely right. It’s very similar but with generic type parameters in functions.
当使用不受约束的泛型时,我们可以编写适用于任何类型的值的函数。在内部,它们的行为类似于unknown,这意味着我们可以执行类型保护来缩小类型。最大的区别是,一旦我们使用函数,我们就会用真实类型替换泛型,而不会丢失任何有关类型的信息。
When using generics without constraints, we can write functions that work with values of any type. Inside, they behave like unknown, which means we can do type guards to narrow the type. The biggest difference is that once we use the function, we substitute our generics with real types, not losing any information on typing at all.
这让我们对类型有了更清晰的认识,而不仅仅是允许一切。此pairs函数接受两个参数并创建一个元组:
This allows us to be a bit clearer with our types than just allowing everything. This pairs function takes two arguments and creates a tuple:
functionpairs(a:unknown,b:unknown):[unknown,unknown]{return[a,b];}consta=pairs(1,"1");// [unknown, unknown]
functionpairs(a:unknown,b:unknown):[unknown,unknown]{return[a,b];}consta=pairs(1,"1");// [unknown, unknown]
通过泛型类型参数,我们得到了一个很好的元组类型:
With generic type parameters, we get a nice tuple type:
functionpairs<T,U>(a:T,b:U):[T,U]{return[a,b];}constb=pairs(1,"1");// [number, string]
functionpairs<T,U>(a:T,b:U):[T,U]{return[a,b];}constb=pairs(1,"1");// [number, string]
使用相同的泛型类型参数,我们可以确保仅获取每个元素都属于同一类型的元组:
Using the same generic type parameter, we can make sure we get tuples only where each element is of the same type:
functionpairs<T>(a:T,b:T):[T,T]{return[a,b];}constc=pairs(1,"1");// ^// Argument of type 'string' is not assignable to parameter of type 'number'
functionpairs<T>(a:T,b:T):[T,T]{return[a,b];}constc=pairs(1,"1");// ^// Argument of type 'string' is not assignable to parameter of type 'number'
那么,你应该到处使用泛型吗?不一定。本章包含许多依赖于在正确的时间获取正确的类型信息的解决方案。当你对更广泛的值集感到满意并且可以依赖子类型兼容时,你根本不需要使用泛型。如果你的代码中有any,unknown请考虑你是否在某个时候需要实际类型。添加泛型类型参数可能会有所帮助。
So, should you use generics everywhere? Not necessarily. This chapter includes many solutions that rely on getting the right type information at the right time. When you are happy with a wider set of values and can rely on subtypes being compatible, you don’t need to use generics at all. If you have any and unknown in your code, think whether you need the actual type at some point. Adding a generic type parameter instead might help.
请记住,泛型类型的值可以(显式或隐式)替换为各种子类型。编写子类型友好的代码。
Remember that values of a generic type can be—explicitly and implicitly—substituted with a variety of subtypes. Write subtype-friendly code.
您为应用程序创建过滤逻辑。您可以使用组合器组合不同的过滤规则。您还可以将常规过滤规则与组合过滤器"and" | "or"的结果链接起来。您可以根据以下
行为创建类型:
You create a filter logic for your application. You have different filter rules that you can combine using "and" | "or" combinators. You can also chain regular filter rules with the outcome of combinatorial filters. You create your types based on this
behavior:
typeFilterRule={field:string;operator:string;value:any;};typeCombinatorialFilter={combinator:"and"|"or";rules:FilterRule[];};typeChainedFilter={rules:(CombinatorialFilter|FilterRule)[];};typeFilter=CombinatorialFilter|ChainedFilter;
typeFilterRule={field:string;operator:string;value:any;};typeCombinatorialFilter={combinator:"and"|"or";rules:FilterRule[];};typeChainedFilter={rules:(CombinatorialFilter|FilterRule)[];};typeFilter=CombinatorialFilter|ChainedFilter;
现在,您要编写一个reset函数,该函数基于已提供的过滤器重置所有规则。您可以使用类型保护来区分CombinatorialFilter和ChainedFilter:
Now you want to write a reset function that, based on an already provided filter, resets all rules. You use type guards to distinguish between CombinatorialFilter and ChainedFilter:
functionreset(filter:Filter):Filter{if("combinator"infilter){// filter is CombinatorialFilterreturn{combinator:"and",rules:[]};}// filter is ChainedFilterreturn{rules:[]};}constfilter:CombinatorialFilter={rules:[],combinator:"or"};constresetFilter=reset(filter);// resetFilter is Filter
functionreset(filter:Filter):Filter{if("combinator"infilter){// filter is CombinatorialFilterreturn{combinator:"and",rules:[]};}// filter is ChainedFilterreturn{rules:[]};}constfilter:CombinatorialFilter={rules:[],combinator:"or"};constresetFilter=reset(filter);// resetFilter is Filter
行为符合您的要求,但返回类型reset太宽泛。当我们传递 时CombinatorialFilter,应确保重置过滤器也是CombinatorialFilter。这里是联合类型,就像我们的函数签名所示。但您想确保如果传递某种类型的过滤器,也会获得相同的返回类型。因此,您用限制为 的泛型类型参数替换宽泛的联合类型Filter。返回类型按预期工作,但函数的实现会抛出错误:
The behavior is what you are after, but the return type of reset is too wide. When we pass a CombinatorialFilter, we should be sure that the reset filter is also a CombinatorialFilter. Here it’s the union type, just like our function signature indicates. But you want to make sure that if you pass a filter of a certain type, you also get the same return type. So you replace the broad union type with a generic type parameter that is constrained to Filter. The return type works as intended, but the implementation of your function throws errors:
functionreset<FextendsFilter>(filter:F):F{if("combinator"infilter){return{combinator:"and",rules:[]};// ^ '{ combinator: "and"; rules: never[]; }' is assignable to// the constraint of type 'F', but 'F' could be instantiated// with a different subtype of constraint 'Filter'.}return{rules:[]};//^ '{ rules: never[]; }' is assignable to the constraint of type 'F',// but 'F' could be instantiated with a different subtype of// constraint 'Filter'.}constresetFilter=reset(filter);// resetFilter is CombinatorialFilter
functionreset<FextendsFilter>(filter:F):F{if("combinator"infilter){return{combinator:"and",rules:[]};// ^ '{ combinator: "and"; rules: never[]; }' is assignable to// the constraint of type 'F', but 'F' could be instantiated// with a different subtype of constraint 'Filter'.}return{rules:[]};//^ '{ rules: never[]; }' is assignable to the constraint of type 'F',// but 'F' could be instantiated with a different subtype of// constraint 'Filter'.}constresetFilter=reset(filter);// resetFilter is CombinatorialFilter
虽然您想区分联合的两个部分,但 TypeScript 的思维更广泛。它知道您可能会传入一个结构上与 兼容的对象Filter,但它具有更多属性,因此是子类型。
While you want to differentiate between two parts of a union, TypeScript thinks more broadly. It knows that you might pass in an object that is structurally compatible with Filter, but it has more properties and is therefore a subtype.
这意味着你可以reset调用F实例化为子类型,并且你的程序会很乐意覆盖所有多余的属性。这是错误的,TypeScript 会告诉你:
This means you can call reset with F instantiated to a subtype, and your program would happily override all excess properties. This is wrong, and TypeScript tells you that:
constonDemandFilter=reset({combinator:"and",rules:[],evaluated:true,result:false,});/* filter is {combinator: "and";rules: never[];evaluated: boolean;result: boolean;}; */
constonDemandFilter=reset({combinator:"and",rules:[],evaluated:true,result:false,});/* filter is {combinator: "and";rules: never[];evaluated: boolean;result: boolean;}; */
通过编写子类型友好的代码来克服这个问题。克隆输入对象(仍然是 类型F),设置需要相应更改的属性,并返回仍然是 类型的内容F:
Overcome this by writing subtype-friendly code. Clone the input object (still type F), set the properties that need to be changed accordingly, and return something that is still of type F:
functionreset<FextendsFilter>(filter:F):F{constresult={...filter};// result is Fresult.rules=[];if("combinator"inresult){result.combinator="and";}returnresult;}constresetFilter=reset(filter);// resetFilter is CombinatorialFilter
functionreset<FextendsFilter>(filter:F):F{constresult={...filter};// result is Fresult.rules=[];if("combinator"inresult){result.combinator="and";}returnresult;}constresetFilter=reset(filter);// resetFilter is CombinatorialFilter
泛型类型可以是联合中的众多类型之一,但它们可以有更多。TypeScript 的结构类型系统允许您处理各种子类型,您的代码需要反映这一点。
Generic types can be one of many in a union, but they can be much, much more. TypeScript’s structural type system allows you to work on a variety of subtypes, and your code needs to reflect that.
这是一个不同的场景,但结果类似。您想要创建一个树数据结构并编写一个存储所有树项的递归类型。此类型可以子
类型化,因此您可以编写一个createRootItem带有泛型类型参数的函数,因为您想使用正确的子类型来实例化它:
Here’s a different scenario but with a similar outcome. You want to create a tree data structure and write a recursive type that stores all tree items. This type can be
subtyped, so you write a createRootItem function with a generic type parameter since you want to instantiate it with the correct subtype:
typeTreeItem={id:string;children:TreeItem[];collapsed?:boolean;};functioncreateRootItem<TextendsTreeItem>():T{return{id:"root",children:[],};// '{ id: string; children: never[]; }' is assignable to the constraint// of type 'T', but 'T' could be instantiated with a different subtype// of constraint 'TreeItem'.(2322)}constroot=createRootItem();// root is TreeItem
typeTreeItem={id:string;children:TreeItem[];collapsed?:boolean;};functioncreateRootItem<TextendsTreeItem>():T{return{id:"root",children:[],};// '{ id: string; children: never[]; }' is assignable to the constraint// of type 'T', but 'T' could be instantiated with a different subtype// of constraint 'TreeItem'.(2322)}constroot=createRootItem();// root is TreeItem
我们得到了与之前类似的错误,因为我们不可能说返回值与所有子类型兼容。要解决这个问题,就去掉泛型吧!我们知道返回类型是什么样子的——它是TreeItem:
We get a similar error as before, since we can’t possibly say that the return value will be compatible with all the subtypes. To solve this problem, get rid of the generic! We know how the return type will look—it’s a TreeItem:
functioncreateRootItem():TreeItem{return{id:"root",children:[],};}
functioncreateRootItem():TreeItem{return{id:"root",children:[],};}
最简单的解决方案往往是更好的解决方案。但现在您想通过将类型或子类型的子项附加TreeItem到新创建的根来扩展您的软件。我们尚未添加任何泛型,并且有些不满意:
The simplest solutions are often the better ones. But now you want to extend your software by being able to attach children of type or subtype TreeItem to a newly created root. We don’t add any generics yet and are somewhat dissatisfied:
functionattachToRoot(children:TreeItem[]):TreeItem{return{id:"root",children,};}constroot=attachToRoot([]);// TreeItem
functionattachToRoot(children:TreeItem[]):TreeItem{return{id:"root",children,};}constroot=attachToRoot([]);// TreeItem
root类型为TreeItem,但我们丢失了有关子类型子项的任何信息。即使我们仅为子项添加一个通用类型参数,限制为TreeItem,我们也不会随时保留此信息:
root is of type TreeItem, but we lose any information about the subtyped children. Even if we add a generic type parameter just for the children, constrained to TreeItem, we don’t retain this information on the go:
functionattachToRoot<TextendsTreeItem>(children:T[]):TreeItem{return{id:"root",children,};}constroot=attachToRoot([{id:"child",children:[],collapsed:false,marked:true,},]);// root is TreeItem
functionattachToRoot<TextendsTreeItem>(children:T[]):TreeItem{return{id:"root",children,};}constroot=attachToRoot([{id:"child",children:[],collapsed:false,marked:true,},]);// root is TreeItem
当我们开始添加泛型类型作为返回类型时,我们遇到了与之前相同的问题。为了解决这个问题,我们需要将根项类型与子项类型分开,将其打开TreeItem为泛型,我们可以将其设置Children为的子类型TreeItem。
When we start adding a generic type as a return type, we run into the same problems as before. To solve this issue, we need to split the root item type from the children item type, by opening up TreeItem to be a generic, where we can set Children to be a subtype of TreeItem.
由于我们要避免任何循环引用,我们需要设置Children为默认值BaseTreeItem,因此我们可以将两者都用作和的TreeItem约束:ChildrenattachToRoot
Since we want to avoid any circular references, we need to set Children to a default BaseTreeItem, so we can use TreeItem both as a constraint for Children and for attachToRoot:
typeBaseTreeItem={id:string;children:BaseTreeItem[];};typeTreeItem<ChildrenextendsTreeItem=BaseTreeItem>={id:string;children:Children[];collapsed?:boolean;};functionattachToRoot<TextendsTreeItem>(children:T[]):TreeItem<T>{return{id:"root",children,};}constroot=attachToRoot([{id:"child",children:[],collapsed:false,marked:true,},]);/*root is TreeItem<{id: string;children: never[];collapsed: false;marked: boolean;}>*/
typeBaseTreeItem={id:string;children:BaseTreeItem[];};typeTreeItem<ChildrenextendsTreeItem=BaseTreeItem>={id:string;children:Children[];collapsed?:boolean;};functionattachToRoot<TextendsTreeItem>(children:T[]):TreeItem<T>{return{id:"root",children,};}constroot=attachToRoot([{id:"child",children:[],collapsed:false,marked:true,},]);/*root is TreeItem<{id: string;children: never[];collapsed: false;marked: boolean;}>*/
再次,我们编写子类型友好并将我们的输入参数视为它们自己的,而不是做出假设。
Again, we write subtype friendly and treat our input parameters as their own, instead of making assumptions.
使用通用映射类型根据原始类型创建新的对象类型。
Use generic mapped types to create new object types based on the original type.
让我们回到3.1 节中的玩具店。借助联合类型、交集类型和可区分联合类型,我们能够很好地对数据进行建模:
Let’s go back to the toy shop from Recipe 3.1. Thanks to union types, intersection types, and discriminated union types, we were able to model our data quite nicely:
typeToyBase={name:string;description:string;minimumAge:number;};typeBoardGame=ToyBase&{kind:"boardgame";players:number;};typePuzzle=ToyBase&{kind:"puzzle";pieces:number;};typeDoll=ToyBase&{kind:"doll";material:"plush"|"plastic";};typeToy=Doll|Puzzle|BoardGame;
typeToyBase={name:string;description:string;minimumAge:number;};typeBoardGame=ToyBase&{kind:"boardgame";players:number;};typePuzzle=ToyBase&{kind:"puzzle";pieces:number;};typeDoll=ToyBase&{kind:"doll";material:"plush"|"plastic";};typeToy=Doll|Puzzle|BoardGame;
在我们的代码中,我们需要将模型中的所有玩具分组到一个数据结构中,该数据结构可以用名为 的类型来描述GroupedToys。每个类别(或)GroupedToys都有一个属性,并且有一个数组作为值。一个函数接受一个未排序的玩具列表并按种类对它们进行分组:"kind"ToygroupToys
Somewhere in our code, we need to group all toys from our model in a data structure that can be described by a type called GroupedToys. GroupedToys has a property for each category (or "kind") and a Toy array as value. A groupToys function takes an unsorted list of toys and groups them by kind:
typeGroupedToys={boardgame:Toy[];puzzle:Toy[];doll:Toy[];};functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={boardgame:[],puzzle:[],doll:[],};for(lettoyoftoys){groups[toy.kind].push(toy);}returngroups;}
typeGroupedToys={boardgame:Toy[];puzzle:Toy[];doll:Toy[];};functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={boardgame:[],puzzle:[],doll:[],};for(lettoyoftoys){groups[toy.kind].push(toy);}returngroups;}
此代码中已经有一些细节。首先,我们在声明时使用显式类型注释groups。这确保我们不会忘记任何类别。此外,由于的键与中GroupedToys的类型并集相同,我们可以轻松地通过进行索引访问。"kind"Toygroupstoy.kind
There are already some niceties in this code. First, we use an explicit type annotation when declaring groups. This ensures we are not forgetting any category. Also, since the keys of GroupedToys are the same as the union of "kind" types in Toy, we can easily index access groups by toy.kind.
几个月过去了,冲刺也过去了,我们需要再次接触我们的模型。玩具店现在正在销售原装或可能来自其他供应商的互锁玩具积木。我们将新类型连接Bricks到我们的Toy模型:
Months and sprints pass, and we need to touch our model again. The toy shop is now selling original or maybe alternate vendors of interlocking toy bricks. We wire the new type Bricks up to our Toy model:
typeBricks=ToyBase&{kind:"bricks",pieces:number;brand:string;}typeToy=Doll|Puzzle|BoardGame|Bricks;
typeBricks=ToyBase&{kind:"bricks",pieces:number;brand:string;}typeToy=Doll|Puzzle|BoardGame|Bricks;
由于也groupToys需要处理Bricks,我们得到一个很好的错误,因为GroupedToys没有关于"bricks"类型的线索:
Since groupToys needs to deal with Bricks, too, we get a nice error because GroupedToys has no clue about a "bricks" kind:
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={boardgame:[],puzzle:[],doll:[],};for(lettoyoftoys){groups[toy.kind].push(toy);// ^- Element implicitly has an 'any' type because expression// of type '"boardgame" | "puzzle" | "doll" | "bricks"' can't// be used to index type 'GroupedToys'.// Property 'bricks' does not exist on type 'GroupedToys'.(7053)}returngroups;}
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={boardgame:[],puzzle:[],doll:[],};for(lettoyoftoys){groups[toy.kind].push(toy);// ^- Element implicitly has an 'any' type because expression// of type '"boardgame" | "puzzle" | "doll" | "bricks"' can't// be used to index type 'GroupedToys'.// Property 'bricks' does not exist on type 'GroupedToys'.(7053)}returngroups;}
这是 TypeScript 中期望的行为:知道何时类型不再匹配。这应该引起我们的注意。让我们给出GroupedToys一个groupToys更新:
This is desired behavior in TypeScript: knowing when types don’t match anymore. This should draw our attention. Let’s give GroupedToys and groupToys an update:
typeGroupedToys={boardgame:Toy[];puzzle:Toy[];doll:Toy[];bricks:Toy[];};functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={boardgame:[],puzzle:[],doll:[],bricks:[],};for(lettoyoftoys){groups[toy.kind].push(toy);}returngroups;}
typeGroupedToys={boardgame:Toy[];puzzle:Toy[];doll:Toy[];bricks:Toy[];};functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={boardgame:[],puzzle:[],doll:[],bricks:[],};for(lettoyoftoys){groups[toy.kind].push(toy);}returngroups;}
有一件麻烦的事情:对玩具进行分组的任务总是一样的。无论我们的模型如何变化,我们总是会按种类进行选择并推送到数组中。我们需要groups对每次变化进行维护,但如果我们改变对分组的看法,我们就可以针对变化进行优化。首先,我们将类型更改GroupedToys为具有可选属性。其次,如果尚未进行任何初始化,我们将用一个空数组初始化每个组:
There is one bothersome thing: the task of grouping toys is always the same. No matter how much our model changes, we will always select by kind and push into an array. We would need to maintain groups with every change, but if we change how we think about groups, we can optimize for change. First, we change the type GroupedToys to feature optional properties. Second, we initialize each group with an empty array if there hasn’t been any initialization yet:
typeGroupedToys={boardgame?:Toy[];puzzle?:Toy[];doll?:Toy[];bricks?:Toy[];};functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){// Initialize when not availablegroups[toy.kind]=groups[toy.kind]??[];groups[toy.kind]?.push(toy);}returngroups;}
typeGroupedToys={boardgame?:Toy[];puzzle?:Toy[];doll?:Toy[];bricks?:Toy[];};functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){// Initialize when not availablegroups[toy.kind]=groups[toy.kind]??[];groups[toy.kind]?.push(toy);}returngroups;}
我们不需要groupToys再维护了。唯一需要维护的是类型GroupedToys。如果我们仔细观察GroupedToys,我们会发现它与存在隐式关系Toy。每个属性键都是的一部分Toy["kind"]。让我们将这种关系明确化。使用映射类型,我们根据中的每种类型创建一个新的对象类型Toy["kind"]。
We don’t need to maintain groupToys anymore. The only thing that needs maintenance is the type GroupedToys. If we look closely at GroupedToys, we see that there is an implicit relation to Toy. Each property key is part of Toy["kind"]. Let’s make this relation explicit. With a mapped type, we create a new object type based on each type in Toy["kind"].
Toy["kind"]是字符串文字的联合:"boardgame" | "puzzle" | "doll" | "bricks"。由于我们有一个非常精简的字符串集,因此此联合的每个元素都将用作其自己的属性键。让我们先理解一下:我们可以使用类型作为新生成的类型的属性键。每个属性都有一个可选的类型修饰符并指向Toy[]:
Toy["kind"] is a union of string literals: "boardgame" | "puzzle" | "doll" | "bricks". Since we have a very reduced set of strings, each element of this union will be used as its own property key. Let that sink in for a moment: we can use a type to be a property key of a newly generated type. Each property has an optional type modifier and points to a Toy[]:
typeGroupedToys={[kinToy["kind"]]?:Toy[];};
typeGroupedToys={[kinToy["kind"]]?:Toy[];};
太棒了!每次我们更改 时Toy,我们都会立即更改Toy[]。我们的代码根本不需要更改;我们仍然可以像以前一样按种类分组。
Fantastic! Every time we change Toy, we immediately change Toy[]. Our code needs no change at all; we can still group by kind as we did before.
这是一种我们有可能推广的模式。让我们创建一个Group类型,它接受一个集合并按特定选择器对其进行分组。我们想要创建一个具有两个类型参数的泛型类型:
This is a pattern we have the potential to generalize. Let’s create a Group type that takes a collection and groups it by a specific selector. We want to create a generic type with two type parameters:
可以Collection是任何东西。
The Collection can be anything.
Selector,的键,Collection因此它可以创建相应的属性。
The Selector, a key of Collection, so it can create the respective properties.
我们的第一个尝试是利用我们已有的内容GroupedToys并用类型参数替换具体类型。这创建了我们所需的内容,但也会导致错误:
Our first attempt would be to take what we had in GroupedToys and replace the concrete types with type parameters. This creates what we need but also causes an error:
// How to use ittypeGroupedToys=Group<Toy,"kind">;typeGroup<Collection,SelectorextendskeyofCollection>={[xinCollection[Selector]]?:Collection[];// ^ Type 'Collection[Selector]' is not assignable// to type 'string | number | symbol'.// Type 'Collection[keyof Collection]' is not// assignable to type 'string | number | symbol'.// Type 'Collection[string] | Collection[number]// | Collection[symbol]' is not assignable to// type 'string | number | symbol'.// Type 'Collection[string]' is not assignable to// type 'string | number | symbol'.(2322)};
// How to use ittypeGroupedToys=Group<Toy,"kind">;typeGroup<Collection,SelectorextendskeyofCollection>={[xinCollection[Selector]]?:Collection[];// ^ Type 'Collection[Selector]' is not assignable// to type 'string | number | symbol'.// Type 'Collection[keyof Collection]' is not// assignable to type 'string | number | symbol'.// Type 'Collection[string] | Collection[number]// | Collection[symbol]' is not assignable to// type 'string | number | symbol'.// Type 'Collection[string]' is not assignable to// type 'string | number | symbol'.(2322)};
TypeScript 警告我们,这Collection[string] | Collection[number] | Collection[symbol]可能会导致任何事情,而不仅仅是可以用作键的东西。这是真的,我们需要为此做好准备。我们有两个选择。
TypeScript warns us that Collection[string] | Collection[number] | Collection[symbol] could result in anything, not just things that can be used as a key. That’s true, and we need to prepare for that. We have two options.
Collection首先,使用指向的类型约束Record<string, any>。Record是一种实用程序类型,它生成一个新对象,其中第一个参数为您提供所有键,第二个参数为您提供类型:
First, use a type constraint on Collection that points to Record<string, any>. Record is a utility type that generates a new object where the first parameter gives you all keys and the second parameter gives you the types:
// This type is built-in!typeRecord<Kextendsstring|number|symbol,T>={[PinK]:T;};
// This type is built-in!typeRecord<Kextendsstring|number|symbol,T>={[PinK]:T;};
这提升Collection为通配符对象,有效地禁用了 的类型检查Groups。这是可以的,因为如果某个东西对于属性键来说是一个不可用的类型,TypeScript 无论如何都会将其丢弃。所以 finalGroup有两个受约束的类型参数:
This elevates Collection to a wildcard object, effectively disabling the type-check from Groups. This is OK because if something would be an unusable type for a property key, TypeScript will throw it away anyway. So the final Group has two constrained type parameters:
typeGroup<CollectionextendsRecord<string,any>,SelectorextendskeyofCollection>={[xinCollection[Selector]]:Collection[];};
typeGroup<CollectionextendsRecord<string,any>,SelectorextendskeyofCollection>={[xinCollection[Selector]]:Collection[];};
第二种选择是检查每个键是否是有效的字符串键。我们可以使用条件类型来查看是否Collection[Selector]是键的有效类型。否则,我们将通过选择来删除此类型never。条件类型本身就很复杂,我们将在方案 5.4中详细解决此问题:
The second option is to do a check for each key to see if it is a valid string key. We can use a conditional type to see if Collection[Selector] is in fact a valid type for a key. Otherwise, we would remove this type by choosing never. Conditional types are their own beast, and we tackle this in Recipe 5.4 extensively:
typeGroup<Collection,SelectorextendskeyofCollection>={[kinCollection[Selector]extendsstring?Collection[Selector]:never]?:Collection[];};
typeGroup<Collection,SelectorextendskeyofCollection>={[kinCollection[Selector]extendsstring?Collection[Selector]:never]?:Collection[];};
请注意,我们确实删除了可选类型修饰符。我们这样做是因为使键成为可选不是分组的任务。我们有另一种类型:Partial<T>,另一种映射类型,使对象类型中的每个属性成为可选的:
Note that we did remove the optional type modifier. We do this because making keys optional is not the task of grouping. We have another type for that: Partial<T>, another mapped type that makes every property in an object type optional:
// This type is built-in!typePartial<T>={[PinkeyofT]?:T[P]};
// This type is built-in!typePartial<T>={[PinkeyofT]?:T[P]};
不管Group你创建哪个助手,现在都可以GroupedToys通过告诉 TypeScript 你想要一个Partial(将所有内容更改为可选属性)的 a GroupofToys来创建一个对象"kind":
No matter which Group helper you create, you can now create a GroupedToys object by telling TypeScript that you want a Partial (changing everything to optional properties) of a Group of Toys by "kind":
typeGroupedToys=Partial<Group<Toy,"kind">>;
typeGroupedToys=Partial<Group<Toy,"kind">>;
使用断言签名来独立于if和switch语句改变类型。
Use assertion signatures to change types independently of if and switch statements.
JavaScript 是一种非常灵活的语言。它的动态类型功能允许您在运行时更改对象,动态添加新属性。开发人员使用此功能。在某些情况下,例如,您会运行元素集合并需要断言某些属性。然后,您存储一个checked属性并将其设置为true,这样您就知道您已通过某个标记:
JavaScript is a very flexible language. Its dynamic typing features allow you to change objects at runtime, adding new properties on the fly. And developers use this. There are situations where you, for example, run over a collection of elements and need to assert certain properties. You then store a checked property and set it to true, just so you know that you passed a certain mark:
functioncheck(person:any){person.checked=true;}constperson={name:"Stefan",age:27,};check(person);// person now has the checked propertyperson.checked;// this is true!
functioncheck(person:any){person.checked=true;}constperson={name:"Stefan",age:27,};check(person);// person now has the checked propertyperson.checked;// this is true!
您希望在类型系统中反映这种行为;否则,即使您确定某些属性存在,您也需要不断地进行额外检查以确认对象中是否存在某些属性。
You want to mirror this behavior in the type system; otherwise, you would need to constantly do extra checks if certain properties are in an object, even though you can be sure that they exist.
断言某些属性存在的一种方法是类型断言。我们说在某个时间点,此属性具有不同的类型:
One way to assert that certain properties exist are, well, type assertions. We say that at a certain point in time, this property has a different type:
(personastypeofperson&{checked:boolean}).checked=true;
(personastypeofperson&{checked:boolean}).checked=true;
很好,但是你需要反复执行这种类型断言,因为它们不会改变 的原始类型person。断言某些属性可用的另一种方法是创建类型谓词,如方案 3.5中所示:
Good, but you would need to do this type assertion over and over again, as they don’t change the original type of person. Another way to assert that certain properties are available is to create type predicates, like those shown in Recipe 3.5:
functioncheck<T>(obj:T):objisT&{checked:true}{(objasT&{checked:boolean}).checked=true;returntrue;}constperson={name:"Stefan",age:27,};if(check(person)){person.checked;// checked is true!}
functioncheck<T>(obj:T):objisT&{checked:true}{(objasT&{checked:boolean}).checked=true;returntrue;}constperson={name:"Stefan",age:27,};if(check(person)){person.checked;// checked is true!}
不过,这种情况有点不同,这会让check函数显得笨拙:你需要true在谓词函数中执行额外的条件并返回。这感觉不对。
This situation is a bit different, though, which makes the check function feel clumsy: you need to do an extra condition and return true in the predicate function. This doesn’t feel right.
值得庆幸的是,TypeScript 还有另一种技术可以用于这种情况:断言签名。断言签名可以在控制流中更改值的类型,而无需条件。它们已针对 Node.jsassert函数建模,该函数接受一个条件,如果条件不成立,则会抛出错误。这意味着,在调用之后assert,您可能会获得比以前更多的信息。例如,如果您调用assert并检查某个值是否具有类型string,那么您就知道在此assert函数之后该值应该是string:
Thankfully, TypeScript has another technique we can leverage in situations like this: assertion signatures. Assertion signatures can change the type of a value in control flow, without the need for conditionals. They have been modeled for the Node.js assert function, which takes a condition, and it throws an error if it isn’t true. This means that, after calling assert, you might have more information than before. For example, if you call assert and check if a value has a type of string, you know that after this assert function the value should be string:
functionassert(condition:any,msg?:string):assertscondition{if(!condition){thrownewError(msg);}}functionyell(str:any){assert(typeofstr==="string");// str is stringreturnstr.toUpperCase();}
functionassert(condition:any,msg?:string):assertscondition{if(!condition){thrownewError(msg);}}functionyell(str:any){assert(typeofstr==="string");// str is stringreturnstr.toUpperCase();}
请注意,如果条件为假,该函数将短路。它会抛出错误,即never案例。如果此函数通过,您就可以真正断言条件。
Please note that the function short-circuits if the condition is false. It throws an error, the never case. If this function passes, you can really assert the condition.
虽然断言签名已针对 Node.js 断言函数建模,但您可以断言任何您喜欢的类型。例如,您可以有一个函数,它接受任何值作为加法,但您断言这些值必须为number:
While assertion signatures have been modeled for the Node.js assert function, you can assert any type you like. For example, you can have a function that takes any value for an addition, but you assert that the values need to be number to continue:
functionassertNumber(val:any):assertsvalisnumber{if(typeofval!=="number"){throwError("value is not a number");}}functionadd(x:unknown,y:unknown):number{assertNumber(x);// x is numberassertNumber(y);// y is numberreturnx+y;}
functionassertNumber(val:any):assertsvalisnumber{if(typeofval!=="number"){throwError("value is not a number");}}functionadd(x:unknown,y:unknown):number{assertNumber(x);// x is numberassertNumber(y);// y is numberreturnx+y;}
您在断言签名中找到的所有示例都是基于断言之后的,并且会用错误短路。但是我们可以采用相同的技术来告诉 TypeScript 有更多属性可用。我们编写一个check与之前的谓词函数非常相似的函数,但这次我们不需要返回true。我们设置属性,并且由于对象在 JavaScript 中是通过值传递的,因此我们可以断言,在调用此函数后,我们传递的任何内容都具有属性checked,即true:
All the examples you find on assertion signatures are based after assertions and short-circuit with errors. But we can take the same technique to tell TypeScript that more properties are available. We write a function that is very similar to check in the predicate function before, but this time we don’t need to return true. We set the property, and since objects are passed by value in JavaScript, we can assert that after calling this function whatever we pass has a property checked, which is true:
functioncheck<T>(obj:T):assertsobjisT&{checked:true}{(objasT&{checked:boolean}).checked=true;}constperson={name:"Stefan",age:27,};check(person);
functioncheck<T>(obj:T):assertsobjisT&{checked:true}{(objasT&{checked:boolean}).checked=true;}constperson={name:"Stefan",age:27,};check(person);
这样,我们就可以动态修改值的类型。这是一项鲜为人知的技术,但能帮到你很多。
And with that, we can modify a value’s type on the fly. It’s a little-known technique that can help you a lot.
将所有子类型存储在类型映射中,通过索引访问扩展,并使用映射类型,例如Partial<T>。
Store all subtypes in a type map, widen with index access, and use mapped types like Partial<T>.
如果您想根据一些基本信息创建复杂对象的变体,工厂函数非常有用。您可能从浏览器 JavaScript 中了解到的一个场景是创建元素。该document.createElement函数接受元素的标签名称,然后您会得到一个对象,您可以在其中修改所有必要的
属性。
Factory functions are great if you want to create variants of complex objects based on some basic information. One scenario that you might know from browser JavaScript is the creation of elements. The document.createElement function accepts an element’s tag name, and you get an object where you can modify all necessary
properties.
您想使用一个简洁的工厂函数为该作品增添趣味,该函数名为createElement。它不仅接受元素的标签名称,还会生成一个属性列表,这样您就无需单独设置每个属性:
You want to spice up this creation with a neat factory function you call createElement. Not only does it take the element’s tag name, but it also makes a list of properties so you don’t need to set each property individually:
// Using create Element// a is HTMLAnchorElementconsta=createElement("a",{href:"https://fettblog.eu"});// b is HTMLVideoElementconstb=createElement("video",{src:"/movie.mp4",autoplay:true});// c is HTMLElementconstc=createElement("my-element");
// Using create Element// a is HTMLAnchorElementconsta=createElement("a",{href:"https://fettblog.eu"});// b is HTMLVideoElementconstb=createElement("video",{src:"/movie.mp4",autoplay:true});// c is HTMLElementconstc=createElement("my-element");
您想为此创建良好的类型,因此您需要注意两件事:
You want to create good types for this, so you need to take care of two things:
确保您只创建有效的 HTML 元素。
Make sure you create only valid HTML elements.
提供一种接受 HTML 元素属性子集的类型。
Provide a type that accepts a subset of an HTML element’s properties.
首先,我们来处理有效的 HTML 元素。大约有 140 个可能的 HTML 元素,数量非常多。每个元素都有一个标签名称(可以表示为字符串)以及 DOM 中的相应原型对象。使用tsconfig.json中的dom库,TypeScript 会以类型形式获取有关这些原型对象的信息。您可以找出所有 140 个元素名称。
Let’s take care of the valid HTML elements first. There are around 140 possible HTML elements, which is a lot. Each of those elements has a tag name, which can be represented as a string, and a respective prototype object in the DOM. Using the dom lib in your tsconfig.json, TypeScript has information on those prototype objects in the form of types. And you can figure out all 140 element names.
提供元素标记名称和原型对象之间映射的一个好方法是使用类型映射。类型映射是一种技术,您可以采用类型别名或接口并让键指向相应的类型变体。然后,您可以使用字符串文字类型的索引访问来获取正确的类型变体:
A good way to provide a mapping between element tag names and prototype objects is to use a type map. A type map is a technique where you take a type alias or interface and let keys point to the respective type variants. You can then get the correct type variant using index access of a string literal type:
typeAllElements={a:HTMLAnchorElement;div:HTMLDivElement;video:HTMLVideoElement;//... and ~140 more!};// HTMLAnchorElementtypeA=AllElements["a"];
typeAllElements={a:HTMLAnchorElement;div:HTMLDivElement;video:HTMLVideoElement;//... and ~140 more!};// HTMLAnchorElementtypeA=AllElements["a"];
它看起来像使用索引访问 JavaScript 对象的属性,但请记住,我们仍在类型级别上工作。这意味着索引访问可以很广泛:
It looks like accessing a JavaScript object’s properties using index access, but remember that we’re still working on a type level. This means index access can be broad:
typeAllElements={a:HTMLAnchorElement;div:HTMLDivElement;video:HTMLVideoElement;//... and ~140 more!};// HTMLAnchorElement | HTMLDivELementtypeAandDiv=AllElements["a"|"div"];
typeAllElements={a:HTMLAnchorElement;div:HTMLDivElement;video:HTMLVideoElement;//... and ~140 more!};// HTMLAnchorElement | HTMLDivELementtypeAandDiv=AllElements["a"|"div"];
让我们使用此映射来键入createElement函数。我们使用一个限制为所有键的通用类型参数AllElements,这使我们能够仅传递有效的 HTML 元素:
Let’s use this map to type the createElement function. We use a generic type parameter constrained to all keys of AllElements, which allows us to pass only valid HTML elements:
functioncreateElement<TextendskeyofAllElements>(tag:T):AllElements[T]{returndocument.createElement(tagasstring)asAllElements[T];}// a is HTMLAnchorElementconsta=createElement("a");
functioncreateElement<TextendskeyofAllElements>(tag:T):AllElements[T]{returndocument.createElement(tagasstring)asAllElements[T];}// a is HTMLAnchorElementconsta=createElement("a");
在这里使用泛型将字符串文字固定到文字类型,我们可以使用它从类型映射中索引正确的 HTML 元素变体。另请注意,使用document.createElement需要两个类型断言。一个使集合更宽(T至string),另一个使集合更窄(HTMLElement至AllElements[T])。这两个断言都表明我们必须处理我们无法控制的 API,如方案 3.9中所述。我们稍后会处理断言。
Use generics here to pin a string literal to a literal type, which we can use to index the right HTML element variant from the type map. Also note that using document.createElement requires two type assertions. One makes the set wider (T to string), and one makes the set narrower (HTMLElement to AllElements[T]). Both assertions indicate that we have to deal with an API outside our control, as established in Recipe 3.9. We will deal with the assertions later on.
现在,我们希望提供为上述 HTML 元素传递额外属性的选项,将 设置href为HTMLAnchorElement,等等。所有属性都已存在于相应的HTMLElement变体中,但它们是必需的,而不是可选的。我们可以使用内置类型 使所有属性都成为可选的Partial<T>。它是一种映射类型,它采用特定类型的所有属性并添加类型修饰符:
Now we want to provide the option to pass extra properties for said HTML elements, to set an href to an HTMLAnchorElement, and so forth. All properties are already in the respective HTMLElement variants, but they’re required, not optional.
We can make all properties optional with the built-in type Partial<T>. It’s a mapped type that takes all properties of a certain type and adds a type modifier:
typePartial<T>={[PinkeyofT]?:T[P]};
typePartial<T>={[PinkeyofT]?:T[P]};
我们用一个可选参数来扩展我们的函数props,该参数是Partial来自 的索引元素AllElements。这样,我们知道如果我们传递"a",我们只能设置在 中可用的属性HTMLAnchorElement:
We extend our function with an optional argument props that is a Partial of the indexed element from AllElements. This way, we know that if we pass an "a", we can only set properties that are available in HTMLAnchorElement:
functioncreateElement<TextendskeyofAllElements>(tag:T,props?:Partial<AllElements[T]>):AllElements[T]{constelem=document.createElement(tagasstring)asAllElements[T];returnObject.assign(elem,props);}consta=createElement("a",{href:"https://fettblog.eu"});constx=createElement("a",{src:"https://fettblog.eu"});// ^--// Argument of type '{ src: string; }' is not assignable to parameter// of type 'Partial<HTMLAnchorElement>'.// Object literal may only specify known properties, and 'src' does not// exist in type 'Partial<HTMLAnchorElement>'.(2345)
functioncreateElement<TextendskeyofAllElements>(tag:T,props?:Partial<AllElements[T]>):AllElements[T]{constelem=document.createElement(tagasstring)asAllElements[T];returnObject.assign(elem,props);}consta=createElement("a",{href:"https://fettblog.eu"});constx=createElement("a",{src:"https://fettblog.eu"});// ^--// Argument of type '{ src: string; }' is not assignable to parameter// of type 'Partial<HTMLAnchorElement>'.// Object literal may only specify known properties, and 'src' does not// exist in type 'Partial<HTMLAnchorElement>'.(2345)
太棒了!现在就看你如何找出所有 140 个 HTML 元素了。或者不用。有人已经完成了这项工作并将其放入lib.dom.tsHTMLElementTagNameMap中。所以让我们改用这个:
Fantastic! Now it’s up to you to figure out all 140 HTML elements. Or not. Somebody already did the work and put HTMLElementTagNameMap into lib.dom.ts. So let’s use this instead:
functioncreateElement<TextendskeyofHTMLElementTagNameMap>(tag:T,props?:Partial<HTMLElementTagNameMap[T]>):HTMLElementTagNameMap[T]{constelem=document.createElement(tag);returnObject.assign(elem,props);}
functioncreateElement<TextendskeyofHTMLElementTagNameMap>(tag:T,props?:Partial<HTMLElementTagNameMap[T]>):HTMLElementTagNameMap[T]{constelem=document.createElement(tag);returnObject.assign(elem,props);}
这也是 使用的接口document.createElement,因此您的工厂函数和内置函数之间不会产生任何冲突。无需额外的断言。
This is also the interface used by document.createElement, so there is no friction between your factory function and the built-in one. No extra assertions necessary.
只有一个警告。您只能使用 提供的 140 个元素HTMLElementTagNameMap。如果您想创建 SVG 元素或可以具有完全自定义元素名称的 Web 组件怎么办?您的工厂函数突然受到
太多限制。
There is only one caveat. You are restricted to the 140 elements provided by HTMLElementTagNameMap. What if you want to create SVG elements, or web components that can have fully customized element names? Your factory function suddenly is
too constrained.
为了允许更多(就像这样document.createElement做一样),我们需要再次将所有可能的字符串添加到混合中。HTMLElementTagNameMap是一个接口。因此,我们可以使用声明合并来扩展具有索引签名的接口,其中我们将所有剩余的字符串映射到HTMLUnknownElement:
To allow for more—as document.createElement does—we would need to add all possible strings to the mix again. HTMLElementTagNameMap is an interface. So we can use declaration merging to extend the interface with an indexed signature, where we map all remaining strings to HTMLUnknownElement:
interfaceHTMLElementTagNameMap{[x:string]:HTMLUnknownElement;};functioncreateElement<TextendskeyofHTMLElementTagNameMap>(tag:T,props?:Partial<HTMLElementTagNameMap[T]>):HTMLElementTagNameMap[T]{constelem=document.createElement(tag);returnObject.assign(elem,props);}// a is HTMLAnchorElementconsta=createElement("a",{href:"https://fettblog.eu"});// b is HTMLUnknownElementconstb=createElement("my-element");
interfaceHTMLElementTagNameMap{[x:string]:HTMLUnknownElement;};functioncreateElement<TextendskeyofHTMLElementTagNameMap>(tag:T,props?:Partial<HTMLElementTagNameMap[T]>):HTMLElementTagNameMap[T]{constelem=document.createElement(tag);returnObject.assign(elem,props);}// a is HTMLAnchorElementconsta=createElement("a",{href:"https://fettblog.eu"});// b is HTMLUnknownElementconstb=createElement("my-element");
现在我们已经拥有了我们想要的一切:
Now we have everything we want:
一个创建类型化 HTML 元素的优秀工厂函数
A great factory function to create typed HTML elements
仅使用一个配置对象即可设置元素属性
The possibility to set element properties with just one configuration object
可以灵活地创建超出定义的元素
The flexibility to create more elements than defined
最后一种很好,但是如果你只想允许使用 Web 组件怎么办?Web 组件有一个惯例;它们的标签名称中必须有一个破折号。我们可以使用字符串模板文字类型的映射类型来对此进行建模。您将在第 6 章中了解有关字符串模板文字类型的所有知识。
The last is great, but what if you only want to allow for web components? Web components have a convention; they need to have a dash in their tag name. We can model this using a mapped type on a string template literal type. You will learn all about string template literal types in Chapter 6.
现在,您唯一需要知道的是,我们创建一组字符串,其中模式是任意字符串,后跟一个破折号,后跟任意字符串。这足以确保我们只传递正确的元素名称。
For now, the only thing you need to know is that we create a set of strings where the pattern is any string followed by a dash followed by any string. This is enough to ensure we only pass correct element names.
映射类型仅适用于类型别名,不适用于接口声明,因此我们需要AllElements再次定义类型:
Mapped types work only with type aliases, not interface declarations, so we need to define an AllElements type again:
typeAllElements=HTMLElementTagNameMap&{[xin`${string}-${string}`]:HTMLElement;};functioncreateElement<TextendskeyofAllElements>(tag:T,props?:Partial<AllElements[T]>):AllElements[T]{constelem=document.createElement(tagasstring)asAllElements[T];returnObject.assign(elem,props);}consta=createElement("a",{href:"https://fettblog.eu"});// OKconstb=createElement("my-element");// OKconstc=createElement("thisWillError");// ^// Argument of type '"thisWillError"' is not// assignable to parameter of type '`${string}-${string}`// | keyof HTMLElementTagNameMap'.(2345)
typeAllElements=HTMLElementTagNameMap&{[xin`${string}-${string}`]:HTMLElement;};functioncreateElement<TextendskeyofAllElements>(tag:T,props?:Partial<AllElements[T]>):AllElements[T]{constelem=document.createElement(tagasstring)asAllElements[T];returnObject.assign(elem,props);}consta=createElement("a",{href:"https://fettblog.eu"});// OKconstb=createElement("my-element");// OKconstc=createElement("thisWillError");// ^// Argument of type '"thisWillError"' is not// assignable to parameter of type '`${string}-${string}`// | keyof HTMLElementTagNameMap'.(2345)
太棒了。有了AllElements类型,我们还可以得到类型断言,但我们不太喜欢这个。在这种情况下,除了断言,我们还可以使用函数重载,定义两个声明:一个用于我们的用户,一个用于我们实现该函数。您可以在配方2.6和12.7中了解有关此函数重载技术的更多信息:
Fantastic. With the AllElements type we also get type assertions back, which we don’t like that much. In that case, instead of asserting, we can also use a function overload, defining two declarations: one for our users, and one for us to implement the function. You can learn more about this function overload technique in Recipes 2.6 and 12.7:
functioncreateElement<TextendskeyofAllElements>(tag:T,props?:Partial<AllElements[T]>):AllElements[T];functioncreateElement(tag:string,props?:Partial<HTMLElement>):HTMLElement{constelem=document.createElement(tag);returnObject.assign(elem,props);}
functioncreateElement<TextendskeyofAllElements>(tag:T,props?:Partial<AllElements[T]>):AllElements[T];functioncreateElement(tag:string,props?:Partial<HTMLElement>):HTMLElement{constelem=document.createElement(tag);returnObject.assign(elem,props);}
一切就绪。我们定义了一个具有映射类型和索引签名的类型映射,并使用通用类型参数明确表达我们的意图。这是我们 TypeScript 工具带中多种工具的完美组合。
We are all set. We defined a type map with mapped types and index signatures, using generic type parameters to be very explicit about our intentions. A great combination of multiple tools in our TypeScript tool belt.
使用内置泛型ThisType<T>来定义正确的this。
Use the built-in generic ThisType<T> to define the correct this.
像VueJS这样的框架很大程度上依赖于工厂函数,您可以在其中传递一个综合配置对象来定义每个实例的初始数据、计算属性和方法。您想为应用程序的组件创建类似的行为。这个想法是提供一个具有三个属性的配置对象:
Frameworks like VueJS rely a lot on factory functions, where you pass a comprehensive configuration object to define initial data, computed properties, and methods for each instance. You want to create a similar behavior for components of your app. The idea is to provide a configuration object with three properties:
datadata function返回值是实例的初始数据。您不应在此函数中访问配置对象的任何其他属性。
The return value is the initial data for the instance. You should not have access to any other properties from the configuration object in this function.
computedcomputed property这是针对基于初始数据的计算属性。计算属性使用函数声明。它们可以像 普通属性一样访问初始数据。
This is for computed properties, which are based on the initial data. Computed properties are declared using functions. They can access initial data just like normal properties.
methodsmethods property可以调用方法并访问计算属性以及初始数据。当方法访问计算属性时,它们会像访问普通属性一样访问它:无需调用函数。
Methods can be called and can access computed properties as well as the initial data. When methods access computed properties, they access it like they would access normal properties: no need to call the function.
查看正在使用的配置对象,有三种不同的解释方式this。在 中data,this根本没有任何属性。在 中,每个函数都可以访问viacomputed的返回值,就像它是其对象的一部分一样。在 中,每个方法都可以以相同的方式访问计算属性和via :datathismethodsdatathis
Looking at the configuration object in use, there are three different ways to interpret this. In data, this doesn’t have any properties at all. In computed, each function can access the return value of data via this just like it would be part of their object. In methods, each method can access computed properties and data via this in the same way:
constinstance=create({data(){return{firstName:"Stefan",lastName:"Baumgartner",};},computed:{fullName(){// has access to the return object of datareturnthis.firstName+" "+this.lastName;},},methods:{hi(){// use computed properties just like normal propertiesalert(this.fullName.toLowerCase());},},});
constinstance=create({data(){return{firstName:"Stefan",lastName:"Baumgartner",};},computed:{fullName(){// has access to the return object of datareturnthis.firstName+" "+this.lastName;},},methods:{hi(){// use computed properties just like normal propertiesalert(this.fullName.toLowerCase());},},});
这种行为很特殊,但并不罕见。对于这样的行为,我们肯定希望依赖良好的类型。
This behavior is special but not uncommon. And with a behavior like that, we definitely want to rely on good types.
在本课中,我们将仅关注类型,而不是实际的实现,因为这超出了本章的范围。
In this lesson we will focus only on the types, not on the actual implementation, as that would exceed this chapter’s scope.
让我们为每个属性创建类型。我们定义一个类型Options,我们将逐步完善它。首先是函数data。data可以由用户定义,因此我们希望data使用泛型类型参数来指定。我们正在寻找的数据由data函数的返回类型指定:
Let’s create types for each property. We define a type Options, which we are going to refine step by step. First is the data function. data can be user defined, so we want to specify data using a generic type parameter. The data we are looking for is specified by the return type of the data function:
typeOptions<Data>={data(this:{})?:Data;};
typeOptions<Data>={data(this:{})?:Data;};
因此,一旦我们在函数中指定实际返回值data,Data占位符就会被替换为真实对象的类型。请注意,我们还将其定义this为指向空对象,这意味着我们无法从配置对象访问任何其他属性。
So once we specify an actual return value in the data function, the Data placeholder gets substituted with the real object’s type. Note that we also define this to point to the empty object, which means that we don’t get access to any other property from the configuration object.
接下来,我们定义computed。computed是一个函数对象。我们添加另一个泛型类型参数,称为 ,Computed并让 的值Computed通过使用来类型化。这里,this更改 的所有属性Data。由于我们无法this像在函数中那样设置data,我们可以使用内置辅助类型ThisType并将其设置为泛型类型参数Data:
Next, we define computed. computed is an object of functions. We add another generic type parameter called Computed and let the value of Computed be typed through usage. Here, this changes to all the properties of Data. Since we can’t set this like we do in the data function, we can use the built-in helper type ThisType and set it to the generic type parameter Data:
typeOptions<Data,Computed>={data(this:{})?:Data;computed?:Computed&ThisType<Data>;};
typeOptions<Data,Computed>={data(this:{})?:Data;computed?:Computed&ThisType<Data>;};
例如,这使我们能够像this.firstName上例一样访问 。最后但并非最不重要的一点是,我们要指定methods。methods再次显得很特殊,因为您不仅可以访问Datavia this,还可以访问所有方法以及所有计算属性作为属性。
This allows us to access, for example, this.firstName, like in the previous example. Last but not least, we want to specify methods. methods is again special, as you are getting access not only to Data via this but also to all methods and to all computed properties as properties.
Computed将所有计算属性保存为函数。但我们需要它们的值,更具体地说,它们的返回值。如果我们fullName通过属性访问来访问,我们希望它是一个string。
Computed holds all computed properties as functions. We would need their value, though—more specifically, their return value. If we access fullName via property access, we expect it to be a string.
为此,我们创建了一个名为 的辅助类型MapFnToProp。它接受一个函数对象类型并将其映射到返回值的类型。内置ReturnType辅助类型非常适合此场景:
For that, we create a helper type called MapFnToProp. It takes a type that is an object of functions and maps it to the return values’ types. The built-in ReturnType helper type is perfect for this scenario:
// An object of functions ...typeFnObj=Record<string,()=>any>;// ... to an object of return typestypeMapFnToProp<FunctionObjextendsFnObj>={[KinkeyofFunctionObj]:ReturnType<FunctionObj[K]>;};
// An object of functions ...typeFnObj=Record<string,()=>any>;// ... to an object of return typestypeMapFnToProp<FunctionObjextendsFnObj>={[KinkeyofFunctionObj]:ReturnType<FunctionObj[K]>;};
我们可以使用MapFnToProp来设置ThisType新添加的泛型类型参数,称为Methods。我们还将Data和添加Methods到混合中。要将Computed泛型类型参数传递给MapFnToProp,需要将其约束为,与 中FnObj第一个参数的约束相同:FunctionObjMapFnToProp
We can use MapFnToProp to set ThisType for a newly added generic type parameter called Methods. We also add Data and Methods to the mix. To pass the Computed generic type parameter to MapFnToProp, it needs to be constrained to FnObj, the same constraint of the first parameter FunctionObj in MapFnToProp:
typeOptions<Data,ComputedextendsFnObj,Methods>={data(this:{})?:Data;computed?:Computed&ThisType<Data>;methods?:Methods&ThisType<Data&MapFnToProp<Computed>&Methods>;};
typeOptions<Data,ComputedextendsFnObj,Methods>={data(this:{})?:Data;computed?:Computed&ThisType<Data>;methods?:Methods&ThisType<Data&MapFnToProp<Computed>&Methods>;};
这就是类型!我们获取所有泛型类型属性并将它们添加到create工厂函数中:
And that’s the type! We take all generic type properties and add them to the create factory function:
declarefunctioncreate<Data,ComputedextendsFnObj,Methods>(options:Options<Data,Computed,Methods>):any;
declarefunctioncreate<Data,ComputedextendsFnObj,Methods>(options:Options<Data,Computed,Methods>):any;
通过使用,所有泛型类型参数都将被替换。并且方法Options已输入,我们获得了所有必要的自动完成功能,以确保我们不会遇到麻烦,如图4-1所示。
Through usage, all generic type parameters will be substituted. And the way Options is typed, we get all the autocomplete necessary to ensure we don’t run into troubles, as seen in Figure 4-1.
这个例子完美地展示了如何使用 TypeScript 来编写复杂的 API,其中底层会发生大量的对象操作. 1
This example shows wonderfully how TypeScript can be used to type elaborate APIs where a lot of object manipulation is happening underneath.1
当您将复杂的文字值传递给函数时,TypeScript 会将类型扩展为更通用的类型。虽然这在很多情况下都是理想的行为,但在某些情况下,您希望处理文字类型而不是扩展类型。
When you pass complex, literal values to a function, TypeScript widens the type to something more general. While this is desired behavior in a lot of cases, in some you want to work on the literal types rather than the widened type.
const在泛型类型参数前面
添加一个修饰符,以将传递的值保存在const 上下文中。
Add a const modifier in front of your generic type parameter to keep the passed
values in const context.
单页应用 (SPA) 框架往往会用 JavaScript 重新实现许多浏览器功能。例如,History API等功能可以覆盖常规导航行为,SPA 框架会通过交换页面内容和更改浏览器中的 URL 来切换页面,而无需真正重新加载页面。
Single-page application (SPA) frameworks tend to reimplement a lot of browser functionality in JavaScript. For example, features like the History API made it possible to override the regular navigation behavior, which SPA frameworks use to switch between pages without a real page reload, by swapping the content of the page and changing the URL in the browser.
想象一下在一个简约的 SPA 框架上工作,该框架使用所谓的路由器在页面之间导航。页面被定义为组件,并且ComponentConstructor界面知道如何在你的网站上实例化和呈现新元素:
Imagine working on a minimalistic SPA framework that uses a so-called router to navigate between pages. Pages are defined as components, and a ComponentConstructor interface knows how to instantiate and render new elements on your website:
interfaceComponentConstructor{new():Component;}interfaceComponent{render():HTMLElement;}
interfaceComponentConstructor{new():Component;}interfaceComponent{render():HTMLElement;}
路由器应采用组件和相关路径的列表,存储为。通过函数创建路由器时,它应返回一个可让您获得所需路径的对象:stringrouternavigate
The router should take a list of components and associated paths, stored as string. When creating a router through the router function, it should return an object that lets you navigate the desired path:
typeRoute={path:string;component:ComponentConstructor;};functionrouter(routes:Route[]){return{navigate(path:string){// ...},};}
typeRoute={path:string;component:ComponentConstructor;};functionrouter(routes:Route[]){return{navigate(path:string){// ...},};}
我们现在并不关心实际的导航如何实现;相反,我们想关注函数接口的类型。
How the actual navigation is implemented is of no concern to us right now; instead, we want to focus on the typings of the function interface.
路由器按预期工作;它接受一个Route对象数组并返回一个具有navigate函数的对象,这使我们能够触发从一个 URL 到另一个 URL 的导航并呈现新组件:
The router works as intended; it takes an array of Route objects and returns an object with a navigate function, which allows us to trigger the navigation from one URL to the other and renders the new component:
constrtr=router([{path:"/",component:Main,},{path:"/about",component:About,},])rtr.navigate("/faq");
constrtr=router([{path:"/",component:Main,},{path:"/about",component:About,},])rtr.navigate("/faq");
您立即看到的是类型太广泛了。如果我们允许导航到所有string可用信息,那么没有什么可以阻止我们使用无处可去的虚假路线。我们需要为已经准备好并可用的信息实现某种错误处理。那么为什么不使用它呢?
What you immediately see is that the types are way too broad. If we allow navigating to every string available, nothing keeps us from using bogus routes that lead nowhere. We would need to implement some sort of error handling for information that is already ready and available. So why not use it?
我们的第一个想法是用泛型类型参数替换具体类型。TypeScript 处理泛型替换的方式是,如果我们有一个文字类型,TypeScript 将相应地进行子类型化。引入TforRoute并使用T["path"]Instead ofstring接近我们想要实现的目标:
Our first idea would be to replace the concrete type with a generic type parameter. The way TypeScript deals with generic substitution is that if we have a literal type, TypeScript will subtype accordingly. Introducing T for Route and using T["path"] instead of string comes close to what we want to achieve:
functionrouter<TextendsRoute>(routes:T[]){return{navigate(path:T["path"]){// ...},};}
functionrouter<TextendsRoute>(routes:T[]){return{navigate(path:T["path"]){// ...},};}
理论上,这应该可行。如果我们回想一下 TypeScript 在这种情况下对文字、原始类型做了什么,我们会期望该值缩小到文字类型:
In theory, this should work. If we remind ourselves what TypeScript does with literal, primitives types in that case, we would expect the value to be narrowed to the literal type:
functiongetPath<Textendsstring>(route:T):T{returnroute;}constpath=getPath("/");// "/"
functiongetPath<Textendsstring>(route:T):T{returnroute;}constpath=getPath("/");// "/"
你可以在4.3 节中阅读更多内容。一个重要的细节是,path在上例中, 是const 上下文,因为返回的值是不可变的。
You can read more on that in Recipe 4.3. One important detail is that path in the previous example is in a const context, because the returned value is immutable.
唯一的问题是我们处理的是对象和数组,而 TypeScript 倾向于将对象和数组的类型扩展为更通用的类型,以允许值的可变性。如果我们看一个类似的例子,但有一个嵌套对象,我们会看到 TypeScript 采用更广泛的类型:
The only problem is that we are working with objects and arrays, and TypeScript tends to widen types in objects and arrays to something more general to allow for the mutability of values. If we look at a similar example, but with a nested object, we see that TypeScript takes the broader type instead:
typeRoutes={paths:string[];};functiongetPaths<TextendsRoutes>(routes:T):T["paths"]{returnroutes.paths;}constpaths=getPaths({paths:["/","/about"]});// string[]
typeRoutes={paths:string[];};functiongetPaths<TextendsRoutes>(routes:T):T["paths"]{returnroutes.paths;}constpaths=getPaths({paths:["/","/about"]});// string[]
对于对象来说,const 上下文仅paths用于绑定变量,而不用于绑定其内容。这最终会导致我们丢失正确输入所需的一些信息navigate。
For objects, the const context for paths is only for the binding of the variable, not for its contents. This eventually leads to losing some of the information we need to correctly type navigate.
解决此限制的一种方法是手动应用const context,这需要我们将输入参数重新定义为readonly:
A way to work around this limitation is to manually apply const context, which needs us to redefine the input parameter to be readonly:
functionrouter<TextendsRoute>(routes:readonlyT[]){return{navigate(path:T["path"]){history.pushState({},"",path);},};}constrtr=router([{path:"/",component:Main,},{path:"/about",component:About,},]asconst);rtr.navigate("/about");
functionrouter<TextendsRoute>(routes:readonlyT[]){return{navigate(path:T["path"]){history.pushState({},"",path);},};}constrtr=router([{path:"/",component:Main,},{path:"/about",component:About,},]asconst);rtr.navigate("/about");
这很有效,但也要求我们在编码时不要忘记一个非常重要的细节。而积极地记住变通方法总是会导致灾难。
This works but also requires that we not forget a very important detail when coding. And actively remembering workarounds is always a recipe for disaster.
值得庆幸的是,TypeScript 允许我们从泛型类型参数中请求const 上下文。我们不是将其应用于值,而是将泛型类型参数替换为具体值,但在const 上下文中,通过const向泛型类型参数添加修饰符:
Thankfully, TypeScript allows us to request const context from generic type parameters. Instead of applying it to the value, we substitute the generic type parameter for a concrete value but in const context by adding the const modifier to the generic type parameter:
functionrouter<constTextendsRoute>(routes:T[]){return{navigate(path:T["path"]){// tbd},};}
functionrouter<constTextendsRoute>(routes:T[]){return{navigate(path:T["path"]){// tbd},};}
然后,我们就可以像往常一样使用路由器,甚至可以自动完成可能的路径:
We can then use our router just as we are accustomed to and even get autocomplete for possible paths:
constrtr=router([{path:"/",component:Main,},{path:"/about",component:About,},])rtr.navigate("/about");
constrtr=router([{path:"/",component:Main,},{path:"/about",component:About,},])rtr.navigate("/about");
更好的是,当我们传递一些虚假的东西时,我们会得到正确的错误:
Even better, we get proper errors when we pass in something bogus:
constrtr=router([{path:"/",component:Main,},{path:"/about",component:About,},])rtr.navigate("/faq");// ^// Argument of type '"/faq"' is not assignable to// parameter of type '"/" | "/about"'.(2345)
constrtr=router([{path:"/",component:Main,},{path:"/about",component:About,},])rtr.navigate("/faq");// ^// Argument of type '"/faq"' is not assignable to// parameter of type '"/" | "/about"'.(2345)
美妙之处在于:这一切都隐藏在函数的 API 中。我们的期望变得更加清晰,接口告诉我们约束,我们在使用时无需做任何额外的事情router来确保类型安全。
The beautiful thing: it’s all hidden in the function’s API. What we expect becomes clearer, the interface tells us the constraints, and we don’t have to do anything extra when using router to ensure type safety.
1特别感谢Type Challenges的创建者提供这个精美的示例。
1 Special thanks to the creators of Type Challenges for this beautiful example.
在本章中,我们将仔细研究 TypeScript 独有的功能:条件类型。条件类型允许我们根据子类型检查选择类型,从而使我们能够在类型空间中移动,并在设计接口和函数签名方面获得更大的灵活性。
In this chapter, we will take a good look at a feature that is unique to TypeScript: conditional types. Conditional types allow us to select types based on subtype checks, allowing us to move around in the type space and get even more flexibility in how we want to design interfaces and function signatures.
条件类型是一种强大的工具,可让您即时创建类型。它使 TypeScript 的类型系统图灵完备,如GitHub 问题所示,这既出色又有点令人恐惧。拥有如此强大的能力,您很容易忘记自己真正需要哪种类型,从而导致您陷入死胡同或制作难以阅读的类型。在本书中,我们将彻底讨论条件类型的用法,并始终重新评估我们所做的事情是否真正实现了我们期望的目标。
Conditional types are a powerful tool that allows you to make up types on the fly. It makes TypeScript’s type system turing complete, as shown in this GitHub issue, which is both outstanding but also a bit frightening. With so much power in your hands, it’s easy to lose focus on which types you actually need, leading you into dead ends or crafting types that are too hard to read. Throughout this book, we will discuss the usage of conditional types thoroughly, always reassessing that what we do actually leads to our desired goal.
请注意,本章比其他章节短得多。这并不是因为关于条件类型没有太多内容可讲:恰恰相反。这更多的是因为我们将在后续章节中看到条件类型的良好用法。在这里,我们希望专注于基础知识并建立术语,以便在需要某些类型魔法时可以使用和参考。
Note that this chapter is much shorter than others. This is not because there’s not a lot to say about conditional types: quite the contrary. It’s more because we will see good use of conditional types in the subsequent chapters. Here, we want to focus on the fundamentals and establish terminology that you can use and refer to whenever you need some type magic.
使用条件类型定义一组参数和返回类型的规则。
Use conditional types to define a set of rules for parameter and return types.
您可以创建软件,根据用户定义的输入将某些属性显示为标签。您可以区分StringLabel和NumberLabel以允许不同类型的过滤操作和搜索:
You create software that presents certain attributes as labels based on user-defined input. You distinguish between StringLabel and NumberLabel to allow for different kinds of filter operations and searches:
typeStringLabel={name:string;};typeNumberLabel={id:number;};
typeStringLabel={name:string;};typeNumberLabel={id:number;};
用户输入是字符串或数字。该createLabel函数将输入作为原始类型并生成StringLabel或NumberLabel对象:
User input is either a string or a number. The createLabel function takes the input as a primitive type and produces either a StringLabel or NumberLabel object:
functioncreateLabel(input:number|string):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}else{return{name:input};}}
functioncreateLabel(input:number|string):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}else{return{name:input};}}
基本功能完成后,你会发现你的类型太宽泛了。如果你输入number, 的返回类型createLabel仍然是NumberLabel | StringLabel,而它只能是。解决方案是什么?添加函数重载来明确定义类型关系,就像我们在2.6 节NumberLabel中学到的那样:
With the basic functionality done, you see that your types are way too broad. If you enter a number, the return type of createLabel is still NumberLabel | StringLabel, when it can only be NumberLabel. The solution? Adding function overloads to explicitly define type relationships, like we learned in Recipe 2.6:
functioncreateLabel(input:number):NumberLabel;functioncreateLabel(input:string):StringLabel;functioncreateLabel(input:number|string):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}else{return{name:input};}}
functioncreateLabel(input:number):NumberLabel;functioncreateLabel(input:string):StringLabel;functioncreateLabel(input:number|string):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}else{return{name:input};}}
函数重载的工作方式是,重载本身定义使用类型,而最后一个函数声明定义函数体实现的类型。使用createLabel,我们可以传入 astring并获取 aStringLabel或传入 anumber并获取 a NumberLabel,因为这些是外部可用的类型。
The way function overloads work is that the overloads themselves define types for usage, whereas the last function declaration defines the types for the implementation of the function body. With createLabel, we are able to pass in a string and get a StringLabel or pass in a number and get a NumberLabel, as those are the types available to the outside.
如果我们无法预先缩小输入类型,那么这就会很成问题。我们缺少一个外部函数类型,允许我们传入以下number输入string:
This is problematic in cases where we couldn’t narrow the input type beforehand. We lack a function type to the outside that allows us to pass in input that is either number or string:
functioninputToLabel(input:string|number){returncreateLabel(input);// ^// No overload matches this call. (2769)}
functioninputToLabel(input:string|number){returncreateLabel(input);// ^// No overload matches this call. (2769)}
为了解决这个问题,我们添加了另一个重载,它反映了非常广泛的输入类型的实现函数签名:
To circumvent this, we add another overload that mirrors the implementation function signature for very broad input types:
functioncreateLabel(input:number):NumberLabel;functioncreateLabel(input:string):StringLabel;functioncreateLabel(input:number|string):NumberLabel|StringLabel;functioncreateLabel(input:number|string):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}else{return{name:input};}}
functioncreateLabel(input:number):NumberLabel;functioncreateLabel(input:string):StringLabel;functioncreateLabel(input:number|string):NumberLabel|StringLabel;functioncreateLabel(input:number|string):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}else{return{name:input};}}
我们在这里看到的是,我们已经需要三个重载和四个函数签名声明来描述此功能的最基本行为。从那时起,情况就变得更糟了。
What we see here is that we already need three overloads and four function signature declarations total to describe the most basic behavior for this functionality. And from there on, it just gets worse.
我们希望扩展我们的函数,以便能够复制现有的StringLabel和NumberLabel对象。这最终意味着更多的重载:
We want to extend our function to be able to copy existing StringLabel and NumberLabel objects. This ultimately means more overloads:
functioncreateLabel(input:number):NumberLabel;functioncreateLabel(input:string):StringLabel;functioncreateLabel(input:StringLabel):StringLabel;functioncreateLabel(input:NumberLabel):NumberLabel;functioncreateLabel(input:string|StringLabel):StringLabel;functioncreateLabel(input:number|NumberLabel):NumberLabel;functioncreateLabel(input:number|string|StringLabel|NumberLabel):NumberLabel|StringLabel;functioncreateLabel(input:number|string|StringLabel|NumberLabel):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}elseif(typeofinput==="string"){return{name:input};}elseif("id"ininput){return{id:input.id};}else{return{name:input.name};}}
functioncreateLabel(input:number):NumberLabel;functioncreateLabel(input:string):StringLabel;functioncreateLabel(input:StringLabel):StringLabel;functioncreateLabel(input:NumberLabel):NumberLabel;functioncreateLabel(input:string|StringLabel):StringLabel;functioncreateLabel(input:number|NumberLabel):NumberLabel;functioncreateLabel(input:number|string|StringLabel|NumberLabel):NumberLabel|StringLabel;functioncreateLabel(input:number|string|StringLabel|NumberLabel):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}elseif(typeofinput==="string"){return{name:input};}elseif("id"ininput){return{id:input.id};}else{return{name:input.name};}}
说实话,根据我们希望类型提示的表达能力,我们可以编写更少但更多的函数重载。问题仍然很明显:更多的多样性会导致更复杂的函数签名。
Truth be told, depending on how expressive we want our type hints to be, we can write fewer but also a lot more function overloads. The problem is still apparent: more variety results in more complex function signatures.
TypeScript 工具箱中的一种工具可以帮助解决此类情况:条件类型。条件类型允许我们根据某些子类型检查来选择类型。我们询问泛型类型参数是否属于某个子类型,如果是,则从分支返回该类型true,否则从分支返回该类型false。
One tool in TypeScript’s toolbelt can help with situations like this: conditional types. Conditional types allow us to select a type based on certain subtype checks. We ask if a generic type parameter is of a certain subtype and, if so, return the type from the true branch, or otherwise return the type from the false branch.
T例如,如果以下类型是的子类型string(即所有字符串或非常具体的字符串),则返回输入参数。否则,它返回never:
For example, the following type returns the input parameter if T is a subtype of string (which means all strings or very specific ones). Otherwise, it returns never:
typeIsString<T>=Textendsstring?T:never;typeA=IsString<string>;// stringtypeB=IsString<"hello"|"world">;// stringtypeC=IsString<1000>;// never
typeIsString<T>=Textendsstring?T:never;typeA=IsString<string>;// stringtypeB=IsString<"hello"|"world">;// stringtypeC=IsString<1000>;// never
TypeScript 借用了 JavaScript 三元运算符的语法。与 JavaScript三元运算符一样,它会检查某些条件是否有效。但 TypeScript 的类型系统并不像编程语言那样使用典型的条件集,而是仅检查输入类型的值是否包含在我们检查的值集中。
TypeScript borrows this syntax from JavaScript’s ternary operator. And just like JavaScript’s ternary operator, it checks if certain conditions are valid. But instead of having the typical set of conditions you know from a programming language, TypeScript’s type system checks only if the values of the input type are included in the set of values we check against.
使用该工具,我们可以编写一个名为 的条件类型GetLabel<T>。我们检查输入是否是string或StringLabel。如果是,我们返回StringLabel;否则,我们知道它一定是NumberLabel:
With that tool, we are able to write a conditional type called GetLabel<T>. We check if the input is either of string or StringLabel. If so, we return StringLabel; else, we know that it must be a NumberLabel:
typeGetLabel<T>=Textendsstring|StringLabel?StringLabel:NumberLabel;
typeGetLabel<T>=Textendsstring|StringLabel?StringLabel:NumberLabel;
此类型仅检查输入string、StringLabel、number和NumberLabel是否在分支中else。如果我们想要安全起见,我们还可以NumberLabel通过嵌套条件类型来检查可能产生 的输入:
This type only checks if the inputs string, StringLabel, number, and NumberLabel are in the else branch. If we want to be on the safe side, we would also include a check against possible inputs that produce a NumberLabel by nesting conditional types:
typeGetLabel<T>=Textendsstring|StringLabel?StringLabel:Textendsnumber|NumberLabel?NumberLabel:never;
typeGetLabel<T>=Textendsstring|StringLabel?StringLabel:Textendsnumber|NumberLabel?NumberLabel:never;
现在是时候连接我们的泛型了。我们添加一个新的泛型类型参数T,createLabel该参数被限制为所有可能的输入类型。此T参数用作的输入GetLabel<T>,它将产生相应的返回类型:
Now it’s time to wire up our generics. We add a new generic type parameter T to createLabel that is constrained to all possible input types. This T parameter serves as input for GetLabel<T>, where it will produce the respective return type:
functioncreateLabel<Textendsnumber|string|StringLabel|NumberLabel>(input:T):GetLabel<T>{if(typeofinput==="number"){return{id:input}asGetLabel<T>;}elseif(typeofinput==="string"){return{name:input}asGetLabel<T>;}elseif("id"ininput){return{id:input.id}asGetLabel<T>;}else{return{name:input.name}asGetLabel<T>;}}
functioncreateLabel<Textendsnumber|string|StringLabel|NumberLabel>(input:T):GetLabel<T>{if(typeofinput==="number"){return{id:input}asGetLabel<T>;}elseif(typeofinput==="string"){return{name:input}asGetLabel<T>;}elseif("id"ininput){return{id:input.id}asGetLabel<T>;}else{return{name:input.name}asGetLabel<T>;}}
现在我们准备处理所有可能的类型组合,并且仍然可以从中获得正确的返回类型getLabel,只需一行代码即可。
Now we are ready to handle all possible type combinations and will still get the correct return type from getLabel, all in just one line of code.
如果仔细观察,您会发现我们需要解决返回类型的类型检查问题。不幸的是,在使用泛型和条件类型时,TypeScript 无法进行正确的控制流分析。一个小的类型断言告诉 TypeScript 我们正在处理正确的返回类型。
If you look closely, you will see that we needed to work around type-checks for the return type. Unfortunately, TypeScript is not able to do proper control flow analysis when working with generics and conditional types. A little type assertion tells TypeScript that we are dealing with the right return type.
另一种解决方法是将具有条件类型的函数签名视为原始广泛类型函数的重载:
Another workaround would be to think of the function signature with conditional types as an overload to the original broadly typed function:
functioncreateLabel<Textendsnumber|string|StringLabel|NumberLabel>(input:T):GetLabel<T>;functioncreateLabel(input:number|string|StringLabel|NumberLabel):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}elseif(typeofinput==="string"){return{name:input};}elseif("id"ininput){return{id:input.id};}else{return{name:input.name};}}
functioncreateLabel<Textendsnumber|string|StringLabel|NumberLabel>(input:T):GetLabel<T>;functioncreateLabel(input:number|string|StringLabel|NumberLabel):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}elseif(typeofinput==="string"){return{name:input};}elseif("id"ininput){return{id:input.id};}else{return{name:input.name};}}
这样,我们就有了一个灵活的外部世界类型,可以根据输入准确告知我们得到什么输出。至于实现,您可以从广泛的类型中充分了解灵活性。
This way, we have a flexible type for the outside world that tells exactly what output we get based on our input. And for implementation, you have the full flexibility you know from a broad set of types.
这是否意味着在所有情况下你都应该优先选择条件类型而不是函数重载?不一定。在12.7 节中,我们将讨论函数重载是更好选择的情况。
Does this mean you should prefer conditional types over function overloads in all scenarios? Not necessarily. In Recipe 12.7 we look at situations where function overloads are the better choice.
使用分布条件类型来过滤正确的类型。
Use a distributive conditional type to filter for the right type.
假设您的应用程序中有一些遗留代码,您尝试重新创建jQuery之类的框架。您有自己的类型ElementList,它具有辅助函数来向类型对象添加和删除类名HTMLElement,或将事件侦听器绑定到事件。
Let’s say you have some legacy code in your application where you tried to re-create frameworks like jQuery. You have your own kind of ElementList that has helper functions to add and remove class names to objects of type HTMLElement, or to bind event listeners to events.
ElementList此外,您可以通过索引访问来访问列表中的每个元素。可以使用数字索引访问的索引访问类型以及常规字符串属性键来描述此类类型:
Additionally, you can access each element of your list through index access. A type for such an ElementList can be described using an index access type for number index access, together with regular string property keys:
typeElementList={addClass:(className:string)=>ElementList;removeClass:(className:string)=>ElementList;on:(event:string,callback:(ev:Event)=>void)=>ElementList;length:number;[x:number]:HTMLElement;};
typeElementList={addClass:(className:string)=>ElementList;removeClass:(className:string)=>ElementList;on:(event:string,callback:(ev:Event)=>void)=>ElementList;length:number;[x:number]:HTMLElement;};
此数据结构被设计为具有流畅的接口。这意味着如果您调用诸如addClass或 之类的方法removeClass,您将获得相同的对象,因此您可以链接方法调用。
This data structure has been designed to have a fluent interface. Meaning that if you call methods like addClass or removeClass, you get the same object back so you can chain your method calls.
这些方法的示例实现可能如下所示:
A sample implementation of these methods could look like this:
// begin excerptaddClass:function(className:string):ElementList{for(leti=0;i<this.length;i++){this[i].classList.add(className);}returnthis;},removeClass:function(className:string):ElementList{for(leti=0;i<this.length;i++){this[i].classList.remove(className);}returnthis;},on:function(event:string,callback:(ev:Event)=>void):ElementList{for(leti=0;i<this.length;i++){this[i].addEventListener(event,callback);}returnthis;},// end excerpt
// begin excerptaddClass:function(className:string):ElementList{for(leti=0;i<this.length;i++){this[i].classList.add(className);}returnthis;},removeClass:function(className:string):ElementList{for(leti=0;i<this.length;i++){this[i].classList.remove(className);}returnthis;},on:function(event:string,callback:(ev:Event)=>void):ElementList{for(leti=0;i<this.length;i++){this[i].addEventListener(event,callback);}returnthis;},// end excerpt
Array作为或之类的内置集合的扩展NodeList,更改一组HTMLElement对象上的内容变得非常方便:
As an extension of a built-in collection like Array or NodeList, changing things on a set of HTMLElement objects becomes really convenient:
declareconstmyCollection:ElementList;myCollection.addClass("toggle-off").removeClass("toggle-on").on("click",(e)=>{});
declareconstmyCollection:ElementList;myCollection.addClass("toggle-off").removeClass("toggle-on").on("click",(e)=>{});
假设您需要维护jQuery替代品,并发现直接访问元素已被证明有些不安全。当您应用程序的某些部分可以直接更改内容时,您将更难找出更改来自何处,如果不是来自精心设计的ElementList数据结构:
Let’s say you need to maintain your jQuery substitute and figure out that direct element access has proven to be somewhat unsafe. When parts of your application can change things directly, it becomes harder for you to figure out where changes come from, if not from your carefully designed ElementList data structure:
myCollection[1].classList.toggle("toggle-on");
myCollection[1].classList.toggle("toggle-on");
由于无法更改原始库代码(太多部门依赖它),因此您决定将原始代码包装ElementList在Proxy.
Since you can’t change the original library code (too many departments depend on it), you decide to wrap the original ElementList in a Proxy.
Proxy对象采用原始目标对象和定义如何处理访问的处理程序对象。以下实现显示Proxy了仅允许读取访问,并且仅当属性键是类型string而不是字符串(即数字的字符串表示形式)时才允许读取访问:
Proxy objects take an original target object and a handler object that defines how to handle access. The following implementation shows a Proxy that allows only read access, and only if the property key is of type string and not a string that is a string representation of a number:
constsafeAccessCollection=newProxy(myCollection,{get(target,property){if(typeofproperty==="string"&&propertyintarget&&""+parseInt(property)!==property){returntarget[propertyaskeyoftypeoftarget];}returnundefined;},});
constsafeAccessCollection=newProxy(myCollection,{get(target,property){if(typeofproperty==="string"&&propertyintarget&&""+parseInt(property)!==property){returntarget[propertyaskeyoftypeoftarget];}returnundefined;},});
对象中的处理程序对象Proxy仅接收字符串或符号属性。如果您使用数字进行索引访问(例如),0JavaScript 会将其转换为字符串"0"。
Handler objects in Proxy objects receive only string or symbol properties. If you do index access with a number—for example, 0—JavaScript converts this to the string "0".
这在 JavaScript 中运行良好,但我们的类型不再匹配。Proxy构造函数的返回类型ElementList再次为,这意味着数字索引访问仍然完好无损:
This works great in JavaScript, but our types don’t match anymore. The return type of the Proxy constructor is ElementList again, which means that the number index access is still intact:
// Works in TypeScript throws in JavaScriptsafeAccessCollection[0].classList.toggle("toggle-on");
// Works in TypeScript throws in JavaScriptsafeAccessCollection[0].classList.toggle("toggle-on");
我们需要通过定义一个新类型来告诉 TypeScript 我们现在正在处理一个没有数字索引访问的对象。
We need to tell TypeScript that we are now dealing with an object with no number index access by defining a new type.
让我们看看 的键ElementList。如果我们使用keyof运算符,我们将获得 类型对象的所有可能访问方法的联合类型ElementList:
Let’s look at the keys of ElementList. If we use the keyof operator, we get a union type of all possible access methods for objects of type ElementList:
// resolves to "addClass" | "removeClass" | "on" | "length" | numbertypeElementListKeys=keyofElementList;
// resolves to "addClass" | "removeClass" | "on" | "length" | numbertypeElementListKeys=keyofElementList;
它包含四个字符串以及所有可能的数字。现在我们有了这个联合,我们可以创建一个条件类型来删除所有非字符串的内容:
It contains four strings as well as all possible numbers. Now that we have this union, we can create a conditional type that gets rid of everything that isn’t a string:
typeJustStrings<T>=Textendsstring?T:never;
typeJustStrings<T>=Textendsstring?T:never;
JustStrings<T>就是我们所说的分布式条件类型。由于T在条件中是独立的(而不是包装在对象或数组中),TypeScript 会将联合的条件类型视为条件类型的联合。实际上,TypeScript 对联合的每个成员都执行相同的条件检查T。
JustStrings<T> is what we call a distributive conditional type. Since T is on its own in the condition—not wrapped in an object or array—TypeScript will treat a conditional type of a union as a union of conditional types. Effectively, TypeScript does the same conditional check for every member of the union T.
在我们的例子中,它遍历所有成员keyof ElementList:
In our case, it goes through all members of keyof ElementList:
typeJustElementListStrings=|"addClass"extendsstring?"addClass":never|"removeClass"extendsstring?"removeClass":never|"on"extendsstring?"on":never|"length"extendsstring?"length":never|numberextendsstring?number:never;
typeJustElementListStrings=|"addClass"extendsstring?"addClass":never|"removeClass"extendsstring?"removeClass":never|"on"extendsstring?"on":never|"length"extendsstring?"length":never|numberextendsstring?number:never;
跳转到分支的唯一条件false是最后一个条件,我们检查 是否number是 的子类型string,但结果不是。如果我们解决所有条件,我们最终会得到一个新的联合类型:
The only condition that hops into the false branch is the last one, where we check if number is a subtype of string, which it isn’t. If we resolve every condition, we end up with a new union type:
typeJustElementListStrings=|"addClass"|"removeClass"|"on"|"length"|never;
typeJustElementListStrings=|"addClass"|"removeClass"|"on"|"length"|never;
与 的并集never实际上会丢弃never。如果您有一个没有可能值的集合,并将其与一组值连接起来,则这些值将保持不变:
A union with never effectively drops never. If you have a set with no possible value and you join it with a set of values, the values remain:
typeJustElementListStrings=|"addClass"|"removeClass"|"on"|"length";
typeJustElementListStrings=|"addClass"|"removeClass"|"on"|"length";
这正是我们认为可以安全访问的键列表!通过使用辅助类型,我们可以通过选择所有类型的键Pick来创建一个实际上是超类型的类型:ElementListstring
This is exactly the list of keys we consider safe to access! By using the Pick helper type, we can create a type that is effectively a supertype of ElementList by picking all keys that are of type string:
typeSafeAccess=Pick<ElementList,JustStrings<keyofElementList>>;
typeSafeAccess=Pick<ElementList,JustStrings<keyofElementList>>;
如果我们将鼠标悬停在它上面,我们会看到结果类型正是我们想要的:
If we hover over it, we see that the resulting type is exactly what we were looking for:
typeSafeAccess={addClass:(className:string)=>ElementList;removeClass:(className:string)=>ElementList;on:(event:string,callback:(ev:Event)=>void)=>ElementList;length:number;};
typeSafeAccess={addClass:(className:string)=>ElementList;removeClass:(className:string)=>ElementList;on:(event:string,callback:(ev:Event)=>void)=>ElementList;length:number;};
我们将类型作为注释添加到safeAccessCollection。由于可以分配给超类型,因此safeAccessCollection从那一刻起,TypeScript 将被视为没有数字索引访问的类型:
Let’s add the type as an annotation to safeAccessCollection. Since it’s possible to assign to a supertype, TypeScript will treat safeAccessCollection as a type with no number index access from that moment on:
constsafeAccessCollection:Pick<ElementList,JustStrings<keyofElementList>>=newProxy(myCollection,{get(target,property){if(typeofproperty==="string"&&propertyintarget&&""+parseInt(property)!==property){returntarget[propertyaskeyoftypeoftarget];}returnundefined;},});
constsafeAccessCollection:Pick<ElementList,JustStrings<keyofElementList>>=newProxy(myCollection,{get(target,property){if(typeofproperty==="string"&&propertyintarget&&""+parseInt(property)!==property){returntarget[propertyaskeyoftypeoftarget];}returnundefined;},});
当我们尝试访问其中的元素时safeAccessCollection,TypeScript 将会报错:
When we now try to access elements from safeAccessCollection, TypeScript will greet us with an error:
safeAccessCollection[1].classList.toggle("toggle-on");// ^ Element implicitly has an 'any' type because expression of// type '1' can't be used to index type// 'Pick<ElementList, "addClass" | "removeClass" | "on" | "length">'.
safeAccessCollection[1].classList.toggle("toggle-on");// ^ Element implicitly has an 'any' type because expression of// type '1' can't be used to index type// 'Pick<ElementList, "addClass" | "removeClass" | "on" | "length">'.
这正是我们所需要的。分配条件类型的强大之处在于我们可以改变联合的成员。我们将在5.3 节中看到另一个示例,其中我们使用了内置的辅助类型。
And that’s exactly what we need. The power of distributive conditional types is that we change members of a union. We will see another example in Recipe 5.3, where we work with built-in helper types.
Group来自配方 4.5的类型可以正常工作,但是组中每个条目的类型太广泛。
Your Group type from Recipe 4.5 works fine, but the type for each entry of the group is too broad.
使用Extract辅助类型从联合类型中选择正确的成员。
Use the Extract helper type to pick the right member from a union type.
让我们回到食谱3.1和4.5中的玩具店示例。我们从一个精心设计的模型开始,使用可区分的联合类型,我们可以获得有关每个可能值的精确信息:
Let’s go back to the toy shop example from Recipes 3.1 and 4.5. We started with a thoughtfully crafted model, with discriminated union types allowing us to get exact information about every possible value:
typeToyBase={name:string;description:string;minimumAge:number;};typeBoardGame=ToyBase&{kind:"boardgame";players:number;};typePuzzle=ToyBase&{kind:"puzzle";pieces:number;};typeDoll=ToyBase&{kind:"doll";material:"plush"|"plastic";};typeToy=Doll|Puzzle|BoardGame;
typeToyBase={name:string;description:string;minimumAge:number;};typeBoardGame=ToyBase&{kind:"boardgame";players:number;};typePuzzle=ToyBase&{kind:"puzzle";pieces:number;};typeDoll=ToyBase&{kind:"doll";material:"plush"|"plastic";};typeToy=Doll|Puzzle|BoardGame;
然后,我们找到了一种从派生另一种类型的方法,其中我们将属性的联合类型成员作为映射类型的属性键,其中每个属性都是 类型:GroupedToysToykindToy[]
We then found a way to derive another type called GroupedToys from Toy, where we take the union type members of the kind property as property keys for a mapped type, where each property is of type Toy[]:
typeGroupedToys={[kinToy["kind"]]?:Toy[];};
typeGroupedToys={[kinToy["kind"]]?:Toy[];};
借助泛型,我们能够定义一个辅助类型,Group<Collection, Selector>以便在不同场景中重用相同的模式:
Thanks to generics, we were able to define a helper type Group<Collection, Selector> to reuse the same pattern for different scenarios:
typeGroup<CollectionextendsRecord<string,any>,SelectorextendskeyofCollection>={[KinCollection[Selector]]:Collection[];};typeGroupedToys=Partial<Group<Toy,"kind">>;
typeGroup<CollectionextendsRecord<string,any>,SelectorextendskeyofCollection>={[KinCollection[Selector]]:Collection[];};typeGroupedToys=Partial<Group<Toy,"kind">>;
辅助类型效果很好,但有一个警告。如果我们将鼠标悬停在生成的类型上,我们会看到,虽然Group<Collection, Selector>能够Toy正确选择联合类型的判别式,但所有属性都指向一个非常广泛的Toy[]:
The helper type works great, but there’s one caveat. If we hover over the generated type, we see that while Group<Collection, Selector> is able to pick the discriminant of the Toy union type correctly, all properties point to a very broad Toy[]:
typeGroupedToys={boardgame?:Toy[]|undefined;puzzle?:Toy[]|undefined;doll?:Toy[]|undefined;};
typeGroupedToys={boardgame?:Toy[]|undefined;puzzle?:Toy[]|undefined;doll?:Toy[]|undefined;};
但是我们难道不应该知道得更多吗?例如,为什么boardgame指向一个,Toy[]而唯一现实的类型应该是BoardGame[]。拼图和娃娃以及我们想要添加到收藏中的所有后续玩具也是如此。我们期望的类型应该更像这样:
But shouldn’t we know more? For example, why does boardgame point to a Toy[] when the only realistic type should be BoardGame[]. Same for puzzles and dolls, and all the subsequent toys we want to add to our collection. The type we are expecting should look more like this:
typeGroupedToys={boardgame?:BoardGame[]|undefined;puzzle?:Puzzle[]|undefined;doll?:Doll[]|undefined;};
typeGroupedToys={boardgame?:BoardGame[]|undefined;puzzle?:Puzzle[]|undefined;doll?:Doll[]|undefined;};
我们可以通过从联合类型中提取相应的成员来实现此类型Collection。幸运的是,有一个辅助类型可以实现这一点:Extract<T, U>,其中T是集合,U是的一部分T。
We can achieve this type by extracting the respective member from the Collection union type. Thankfully, there is a helper type for that: Extract<T, U>, where T is the collection, U is part of T.
Extract<T, U>定义为:
Extract<T, U> is defined as:
typeExtract<T,U>=TextendsU?T:never;
typeExtract<T,U>=TextendsU?T:never;
就像T条件是裸类型一样,T是分配条件类型,这意味着 TypeScript 检查的每个成员是否T是的子类型U,如果是,它会将此成员保留在联合类型中。这对于从中选择正确的玩具组如何工作Toy?
As T in the condition is a naked type, T is a distributive conditional type, which means TypeScript checks if each member of T is a subtype of U, and if this is the case, it keeps this member in the union type. How would this work for picking the right group of toys from Toy?
假设我们要从Doll中进行选择Toy。Doll有几个属性,但该kind属性与其他属性明显不同。 因此,对于仅查找的类型,Doll意味着我们从 Toy每个类型中提取{ kind: "doll" }:
Let’s say we want to pick Doll from Toy. Doll has a couple of properties, but the kind property separates distinctly from the rest. So for a type to look only for Doll would mean that we extract from Toy every type where { kind: "doll" }:
typeExtractedDoll=Extract<Toy,{kind:"doll"}>;
typeExtractedDoll=Extract<Toy,{kind:"doll"}>;
对于分配条件类型,联合的条件类型是条件类型的联合,因此T将根据 进行检查U:
With distributive conditional types, a conditional type of a union is a union of conditional types, so each member of T is checked against U:
typeExtractedDoll=BoardGameextends{kind:"doll"}?BoardGame:never|Puzzleextends{kind:"doll"}?Puzzle:never|Dollextends{kind:"doll"}?Doll:never;
typeExtractedDoll=BoardGameextends{kind:"doll"}?BoardGame:never|Puzzleextends{kind:"doll"}?Puzzle:never|Dollextends{kind:"doll"}?Doll:never;
和BoardGame都不Puzzle是 的子类型{ kind: "doll" },因此它们解析为never。但是Doll 是 的子类型{ kind: "doll" },因此它解析为Doll:
Both BoardGame and Puzzle are not subtypes of { kind: "doll" }, so they resolve to never. But Doll is a subtype of { kind: "doll" }, so it resolves to Doll:
typeExtractedDoll=never|never|Doll;
typeExtractedDoll=never|never|Doll;
在与 的联合中never,never会消失。因此结果类型为Doll:
In a union with never, never just disappears. So the resulting type is Doll:
typeExtractedDoll=Doll;
typeExtractedDoll=Doll;
这正是我们想要的。让我们将这个检查放入我们的Group辅助类型中。幸运的是,我们拥有从组
集合中提取特定类型的所有部分:
This is exactly what we are looking for. Let’s get that check into our Group helper type. Thankfully, we have all parts available to extract a specific type from a group’s
collection:
本身Collection是一个占位符,最终被替换为Toy
The Collection itself, a placeholder that eventually is substituted with Toy
中的判别性质Selector,最终被替换为"kind"
The discriminant property in Selector, which eventually is substituted with "kind"
我们要提取的判别类型是字符串类型,巧合的是,也是我们映射的属性键Group:K
The discriminant type we want to extract, which is a string type and coincidentally also the property key we map out in Group: K
Extract<Toy, { kind: "doll" }>因此within的通用版本Group<Collection, Selector>是这样的:
So the generic version of Extract<Toy, { kind: "doll" }> within Group<Collection, Selector> is this:
typeGroup<CollectionextendsRecord<string,any>,SelectorextendskeyofCollection>={[KinCollection[Selector]]:Extract<Collection,{[PinSelector]:K}>[];};
typeGroup<CollectionextendsRecord<string,any>,SelectorextendskeyofCollection>={[KinCollection[Selector]]:Extract<Collection,{[PinSelector]:K}>[];};
如果我们Collection用Toy和Selector代替"kind",则类型如下:
If we substitute Collection with Toy and Selector with "kind", the type reads as follows:
[K in Collection[Selector]][K in Collection[Selector]]Toy["kind"]将— 在这种情况下"boardgame",、"puzzle"和—的每个成员"doll"作为新对象类型的属性键。
Take each member of Toy["kind"]—in that case, "boardgame", "puzzle", and "doll"—as a property key for a new object type.
Extract<Collection, …>Extract<Collection, …>Collection从联合类型中提取Toy每个属于...子类型的成员。
Extract from the Collection, the union type Toy, each member that is a subtype of…
{ [P in Selector]: K }{ [P in Selector]: K }遍历每个成员Selector— 在我们的例子中,它只是— 并创建一个对象类型,当属性键为 时,该对象类型"kind"指向,当属性键为 时,等等。"boardgame""boardgame""puzzle""puzzle"
Go through each member of Selector—in our case, it’s just "kind"—and create an object type that points to "boardgame" when the property key is "boardgame", "puzzle" when the property key is "puzzle", and so on.
这就是我们为每个属性键选择正确的成员的方法Toy。结果正如预期的那样:
That’s how we pick for each property key the right member of Toy. The result is as expected:
typeGroupedToys=Partial<Group<Toy,"kind">>;// resolves to:typeGroupedToys={boardgame?:BoardGame[]|undefined;puzzle?:Puzzle[]|undefined;doll?:Doll[]|undefined;};
typeGroupedToys=Partial<Group<Toy,"kind">>;// resolves to:typeGroupedToys={boardgame?:BoardGame[]|undefined;puzzle?:Puzzle[]|undefined;doll?:Doll[]|undefined;};
太棒了!类型现在清晰多了,我们可以确保在选择棋盘游戏时不需要处理谜题。但也出现了一些新 问题。
Fantastic! The type is now a lot clearer, and we can make sure that we don’t need to deal with puzzles when we selected board games. But some new problems have popped up.
由于每个属性的类型都更加精细,并且没有指向非常广泛的Toy类型,TypeScript 在正确解析我们组中的每个集合时遇到了一些困难:
Since the types of each property are much more refined and don’t point to the very broad Toy type, TypeScript struggles a bit with resolving each collection in our group correctly:
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){groups[toy.kind]=groups[toy.kind]??[];// ^ Type 'BoardGame[] | Doll[] | Puzzle[]' is not assignable to// type '(BoardGame[] & Puzzle[] & Doll[]) | undefined'. (2322)groups[toy.kind]?.push(toy);// ^// Argument of type 'Toy' is not assignable to// parameter of type 'never'. (2345)}returngroups;}
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){groups[toy.kind]=groups[toy.kind]??[];// ^ Type 'BoardGame[] | Doll[] | Puzzle[]' is not assignable to// type '(BoardGame[] & Puzzle[] & Doll[]) | undefined'. (2322)groups[toy.kind]?.push(toy);// ^// Argument of type 'Toy' is not assignable to// parameter of type 'never'. (2345)}returngroups;}
问题在于 TypeScript 仍然认为toy可能是所有玩具,而 的每个属性都group指向一些非常具体的玩具。有三种方法可以解决这个问题。
The problem is that TypeScript still thinks of toy as potentially being all toys, whereas each property of group points to some very specific ones. There are three ways to solve this issue.
首先,我们可以再次单独检查每个成员。由于 TypeScript 认为toy它是一种非常广泛的类型,因此缩小范围可以使关系再次清晰:
First, we could again check for each member individually. Since TypeScript thinks of toy as a very broad type, narrowing makes the relationship clear again:
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){switch(toy.kind){case"boardgame":groups[toy.kind]=groups[toy.kind]??[];groups[toy.kind]?.push(toy);break;case"doll":groups[toy.kind]=groups[toy.kind]??[];groups[toy.kind]?.push(toy);break;case"puzzle":groups[toy.kind]=groups[toy.kind]??[];groups[toy.kind]?.push(toy);break;}}returngroups;}
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){switch(toy.kind){case"boardgame":groups[toy.kind]=groups[toy.kind]??[];groups[toy.kind]?.push(toy);break;case"doll":groups[toy.kind]=groups[toy.kind]??[];groups[toy.kind]?.push(toy);break;case"puzzle":groups[toy.kind]=groups[toy.kind]??[];groups[toy.kind]?.push(toy);break;}}returngroups;}
这是可行的,但是我们要避免大量的重复和重复。
That works, but there’s lots of duplication and repetition we want to avoid.
其次,我们可以使用类型断言来扩大类型,groups[toy.kind]以便 TypeScript 可以确保索引访问:
Second, we can use a type assertion to widen the type of groups[toy.kind] so TypeScript can ensure index access:
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){(groups[toy.kind]asToy[])=groups[toy.kind]??[];(groups[toy.kind]asToy[])?.push(toy);}returngroups;}
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){(groups[toy.kind]asToy[])=groups[toy.kind]??[];(groups[toy.kind]asToy[])?.push(toy);}returngroups;}
这实际上就像我们更改为之前一样工作GroupedToys,并且类型断言告诉我们,我们故意在此处更改了类型以摆脱类型错误。
This effectively works like before our change to GroupedToys, and the type assertion tells us that we intentionally changed the type here to get rid of type errors.
第三,我们可以使用一些间接方法。toy我们不是直接添加到组中,而是使用辅助函数assign来处理泛型:
Third, we can work with a little indirection. Instead of adding toy directly to a group, we use a helper function assign where we work with generics:
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){assign(groups,toy.kind,toy);}returngroups;}functionassign<TextendsRecord<string,K[]>,K>(groups:T,key:keyofT,value:K){// Initialize when not availablegroups[key]=groups[key]??[];groups[key]?.push(value);}
functiongroupToys(toys:Toy[]):GroupedToys{constgroups:GroupedToys={};for(lettoyoftoys){assign(groups,toy.kind,toy);}returngroups;}functionassign<TextendsRecord<string,K[]>,K>(groups:T,key:keyofT,value:K){// Initialize when not availablegroups[key]=groups[key]??[];groups[key]?.push(value);}
Toy在这里,我们使用 TypeScript 的泛型替换来缩小联合的正确成员:
Here, we narrow the right member of the Toy union by using TypeScript’s generic substitution:
groups是T,a Record<string, K[]>。K[]可能具有潜在的广泛性。
groups is T, a Record<string, K[]>. K[] can be potentially broad.
key与 是相关的T: 的属性键T。
key is in relation to T: a property key of T.
value是 类型K。
value is of type K.
所有三个函数参数都是相互关联的,我们设计类型关系的方式允许我们安全地访问groups[key]和推value送到数组。
All three function parameters are in relation to one another, and the way we designed the type relations allows us to safely access groups[key] and push value to the array.
此外,调用时每个参数的类型都assign满足我们刚刚设置的泛型类型约束。如果你想了解更多关于此技术的信息,请查看第 12.6 节。
Also, the types of each parameter when we call assign fulfill the generic type constraints we just set. If you want to know more about this technique, check out Recipe 12.6.
映射属性键时使用条件类型和类型断言进行过滤。
Filter with conditional types and type assertions when mapping property keys.
TypeScript 允许你基于其他类型创建类型,这样你就可以保持它们的最新状态,而不必维护它们的每一个派生类型。我们在之前的项目中已经看到了示例,例如配方 4.5。在以下场景中,我们希望根据其属性的类型调整现有的对象类型。让我们看一个 的类型Person:
TypeScript allows you to create types based on other types, so you can keep them up to date without maintaining every one of their derivates. We’ve seen examples in earlier items, like Recipe 4.5. In the following scenario, we want to adapt an existing object type based on the types of its properties. Let’s look at a type for Person:
typePerson={name:string;age:number;profession?:string;};
typePerson={name:string;age:number;profession?:string;};
它由两个字符串profession和name和一个数字组成:age。我们想要创建一个仅由字符串类型属性组成的类型:
It consists of two strings—profession and name—and a number: age. We want to create a type that consists only of string type properties:
typePersonStrings={name:string;profession?:string;};
typePersonStrings={name:string;profession?:string;};
TypeScript 已经具有某些辅助类型来处理过滤属性名称。例如,映射类型Pick<T>采用对象键的子集来创建仅包含这些键的新对象:
TypeScript already has certain helper types to deal with filtering property names. For example, the mapped type Pick<T> takes a subset of an object’s keys to create a new object that contains only those keys:
typePick<T,KextendskeyofT>={[PinK]:T[P];}// Only includes "name"typePersonName=Pick<Person,"name">;// Includes "name" and "profession"typePersonStrings=Pick<Person,"name"|"profession">;
typePick<T,KextendskeyofT>={[PinK]:T[P];}// Only includes "name"typePersonName=Pick<Person,"name">;// Includes "name" and "profession"typePersonStrings=Pick<Person,"name"|"profession">;
如果我们想要删除某些属性,我们可以使用Omit<T>,它的工作原理就像Pick<T>我们通过稍微改变的一组属性来映射微小的差异一样,我们删除了不想包含的属性名称:
If we want to remove certain properties, we can use Omit<T>, which works just like Pick<T> with the small difference that we map through a slightly altered set of properties, one where we remove property names that we don’t want to include:
typeOmit<T,Kextendsstring|number|symbol>={[PinExclude<keyofT,K>]:T[P];}// Omits age, thus includes "name" and "profession"typePersonWithoutAge=Omit<Person,"age">;
typeOmit<T,Kextendsstring|number|symbol>={[PinExclude<keyofT,K>]:T[P];}// Omits age, thus includes "name" and "profession"typePersonWithoutAge=Omit<Person,"age">;
为了根据属性的类型而不是名称来选择正确的属性,我们需要创建一个类似的辅助类型,在该类型中,我们可以映射一组动态生成的属性名称,这些属性名称仅指向我们正在寻找的类型。我们从5.2 节中知道,当对联合类型使用条件类型时,我们可以用它never来过滤来自该联合的元素。
To select the right properties based on their type, rather than their name, we would need to create a similar helper type, one where we map a dynamically generated set of property names that point only to the types we are looking for. We know from Recipe 5.2 that when using conditional types over a union type, we can use never to filter elements from this union.
因此,第一种可能性是我们映射所有属性键Person并检查是否Person[K]是我们所需类型的子集。 如果是,我们返回类型;否则,我们返回never:
So a first possibility could be that we map all property keys of Person and check if Person[K] is a subset of our desired type. If so, we return the type; otherwise, we return never:
// Not there yettypePersonStrings={[KinkeyofPerson]:Person[K]extendsstring?Person[K]:never;};
// Not there yettypePersonStrings={[KinkeyofPerson]:Person[K]extendsstring?Person[K]:never;};
这很好,但有一个警告:我们检查的类型不是联合类型,而是映射类型的类型。因此,我们不会过滤属性键,而是获取指向类型的属性never,这意味着我们将完全禁止设置某些属性。
This is good, but it comes with a caveat: the types we are checking are not in a union but are types from a mapped type. So instead of filtering property keys, we would get properties that point to type never, meaning that we would forbid certain properties to be set at all.
另一个想法是将类型设置为undefined,将属性视为可选的,但是,正如我们在3.11 节中了解到的,缺少的属性和未定义的值是不一样的。
Another idea would be to set the type to undefined, treating the property as sort of optional but, as we learned in Recipe 3.11, missing properties and undefined values are not the same.
我们实际上想要做的是删除指向特定类型的属性键。这可以通过将条件放在对象的右侧而不是左侧(创建属性的位置)来实现。
What we actually want to do is drop the property keys that point to a certain type. This can be achieved by putting the condition not on the righthand side of the object but on the lefthand side, where the properties are created.
就像类型一样Omit,我们需要确保我们映射了一组特定的属性。映射时keyof Person,可以使用类型断言更改属性键的类型。就像常规类型断言一样,有一种故障安全机制,这意味着您不能断言它是任何东西:它必须在属性键的边界内。
Just like with the Omit type, we need to make sure that we map over a specific set of properties. When mapping keyof Person, it is possible to change the type of the property key with a type assertion. Just like with regular type assertions, there is a sort of fail-safe mechanism, meaning you just can’t assert it to be anything: it has to be within the boundaries of a property key.
我们想要断言K集合中的一部分Person[K]是 类型string。如果这是真的,我们保留K;否则,我们用 过滤集合的元素never。由于never位于对象的左侧,因此属性被删除:
We want to assert that K part of the set if Person[K] is of type string. If this is true, we keep K; otherwise, we filter the element of the set with never. With never being on the lefthand side of the object, the property gets dropped:
typePersonStrings={[KinkeyofPersonasPerson[K]extendsstring?K:never]:Person[K];};
typePersonStrings={[KinkeyofPersonasPerson[K]extendsstring?K:never]:Person[K];};
这样,我们只选择指向字符串值的属性键。有一个问题:可选字符串属性的类型比常规字符串更广泛,因为
undefined它也包含在可能的值中。使用联合类型可确保可选属性也得到保留:
And with that, we select only property keys that point to string values. There is one catch: optional string properties have a broader type than regular strings, as
undefined is also included as a possible value. Using a union type ensures that optional properties are also kept:
typePersonStrings={[KinkeyofPersonasPerson[K]extendsstring|undefined?K:never]:Person[K];};
typePersonStrings={[KinkeyofPersonasPerson[K]extendsstring|undefined?K:never]:Person[K];};
下一步是使此类型成为通用类型。我们Select<O, T>通过将 和 替换为Person来O创建string一个类型T:
The next step is making this type generic. We create a type Select<O, T> by replacing Person with O and string with T:
typeSelect<O,T>={[KinkeyofOasO[K]extendsT|undefined?K:never]:O[K];};
typeSelect<O,T>={[KinkeyofOasO[K]extendsT|undefined?K:never]:O[K];};
这种新的辅助类型用途广泛。我们可以使用它从我们自己的对象类型中选择特定类型的属性:
This new helper type is versatile. We can use it to select properties of a certain type from our own object types:
typePersonStrings=Select<Person,string>;typePersonNumbers=Select<Person,number>;
typePersonStrings=Select<Person,string>;typePersonNumbers=Select<Person,number>;
但是我们也可以弄清楚,例如,字符串原型中的哪些函数返回数字:
But we can also figure out, for example, which functions in the string prototype return a number:
typeStringFnsReturningNumber=Select<String,(...args:any[])=>number>;
typeStringFnsReturningNumber=Select<String,(...args:any[])=>number>;
逆辅助类型Remove<O, T>,我们想要删除特定类型的属性键,与 非常相似Select<O, T>。唯一的区别是切换条件并在分支never中返回true:
An inverse helper type Remove<O, T>, where we want to remove property keys of a certain type, is very similar to Select<O, T>. The only difference is to switch the condition and return never in the true branch:
typeRemove<O,T>={[KinkeyofOasO[K]extendsT|undefined?never:K]:O[K];};typePersonWithoutStrings=Remove<Person,string>;
typeRemove<O,T>={[KinkeyofOasO[K]extendsT|undefined?never:K]:O[K];};typePersonWithoutStrings=Remove<Person,string>;
如果你创建对象类型的可序列化版本,这将特别有用:
This is especially helpful if you create a serializable version of your object types:
typeUser={name:string;age:number;profession?:string;posts():string[];greeting():string;};typeSerializeableUser=Remove<User,Function>;
typeUser={name:string;age:number;profession?:string;posts():string[];greeting():string;};typeSerializeableUser=Remove<User,Function>;
通过了解在映射键时可以执行条件类型,您突然可以访问各种潜在的辅助类型。有关更多信息,请参阅第 8 章。
By knowing that you can do conditional types while mapping out keys, you suddenly have access to a wide range of potential helper types. More about that in Chapter 8.
您想要创建一个用于对象序列化的类,该类会删除对象的所有不可序列化的属性(如函数)。如果您的对象有一个serialize函数,则序列化器会获取该函数的返回值,而不是自行序列化对象。您该如何键入该类型?
You want to create a class for object serialization, which removes all unserializable properties of an object like functions. If your object has a serialize function, the serializer takes the return value of the function instead of serializing the object on its own. How can you type that?
使用递归条件类型来修改现有对象类型。对于实现的对象serialize,使用infer关键字将泛型返回类型固定为
具体类型。
Use a recursive conditional type to modify the existing object type. For objects that implement serialize, use the infer keyword to pin the generic return type to a
concrete type.
序列化是将数据结构和对象转换为可存储或传输的格式的过程。想象一下获取一个 JavaScript 对象并将其数据存储在磁盘上,然后稍后通过将其反序列化为 JavaScript 来获取它。
Serialization is the process of converting data structures and objects into a format that can be stored or transferred. Think of taking a JavaScript object and storing its data on disk, just to pick it up later by deserializing it again into JavaScript.
JavaScript 对象可以保存任何类型的数据:字符串或数字等基本类型,以及对象等复合类型,甚至函数。函数很有趣,因为它们不包含数据,而是行为:无法很好地序列化的东西。序列化 JavaScript 对象的一种方法是完全摆脱函数。这就是我们在本课中想要实现的。
JavaScript objects can hold any type of data: primitive types like strings or numbers, as well as compound types like objects, and even functions. Functions are interesting as they don’t contain data but behavior: something that can’t be serialized well. One approach to serializing JavaScript objects is to get rid of functions entirely. And this is what we want to implement in this lesson.
我们从一个简单的对象类型开始Person,它包含我们想要存储的常见数据主题:一个人的姓名和年龄。它还有一个hello方法,可以生成一个字符串:
We start with a simple object type Person, which contains the usual subjects of data we want to store: a person’s name and age. It also has a hello method, which produces a string:
typePerson={name:string;age:number;hello:()=>string;};
typePerson={name:string;age:number;hello:()=>string;};
我们想要序列化此类型的对象。一个Serializer类包含一个空构造函数和一个泛型函数serialize。请注意,我们将泛型类型参数添加到serialize类而不是类。这样,我们可以重用serialize不同的对象类型。返回类型指向一个泛型类型Serialize<T>,它将是序列化过程的结果:
We want to serialize objects of this type. A Serializer class contains an empty constructor and a generic function serialize. Note that we add the generic type parameter to serialize and not to the class. That way, we can reuse serialize for different object types. The return type points to a generic type Serialize<T>, which will be the result of the serialization process:
classSerializer{constructor(){}serialize<T>(obj:T):Serialize<T>{// tbd...}}
classSerializer{constructor(){}serialize<T>(obj:T):Serialize<T>{// tbd...}}
稍后我们会处理实现问题。现在我们先关注类型
Serialize<T>。首先想到的办法就是删除函数属性。我们已经在5.4 节Remove<O, T>中定义了一个很有用的类型,因为它的作用就是删除特定类型的属性:
We will take care of the implementation later. For now let’s focus on the
Serialize<T> type. The first idea that comes to mind is to just drop properties that are functions. We already defined a Remove<O, T> type in Recipe 5.4 that comes in handy, as it does exactly that—removes properties that are of a certain type:
typeRemove<O,T>={[KinkeyofOasO[K]extendsT|undefined?never:K]:O[K];};typeSerialize<T>=Remove<T,Function>;
typeRemove<O,T>={[KinkeyofOasO[K]extendsT|undefined?never:K]:O[K];};typeSerialize<T>=Remove<T,Function>;
第一次迭代已经完成,它适用于简单的、单层深度的对象。然而,对象可以很复杂。例如,Person可以嵌套其他对象,而这些对象又可以具有函数:
The first iteration is done, and it works for simple, one-level-deep objects. Objects can be complex, however. For example, Person could nest other objects, which in turn also could have functions:
typePerson={name:string;age:number;profession:{title:string;level:number;printProfession:()=>void;};hello:()=>string;};
typePerson={name:string;age:number;profession:{title:string;level:number;printProfession:()=>void;};hello:()=>string;};
为了解决这个问题,我们需要检查每个属性是否是另一个对象,如果是,则Serialize<T>再次使用该类型。映射类型调用NestSerialization检查条件类型中每个属性是否属于该类型object,并在分支中返回该类型的序列化版本true,并在分支中返回类型本身false:
To solve this, we need to check each property if it is another object, and if so, use the Serialize<T> type again. A mapped type called NestSerialization checks in a conditional type if each property is of type object and returns a serialized version of that type in the true branch and the type itself in the false branch:
typeNestSerialization<T>={[KinkeyofT]:T[K]extendsobject?Serialize<T[K]>:T[K];};
typeNestSerialization<T>={[KinkeyofT]:T[K]extendsobject?Serialize<T[K]>:T[K];};
我们Serialize<T>通过将的原始Remove<T, Function>类型包装Serialize<T>在 中来重新定义NestSerialization,从而有效地创建了一个递归类型:
Serialize<T>使用NestSerialization<T>使用Serialize<T>,依此类推:
We redefine Serialize<T> by wrapping the original Remove<T, Function> type of Serialize<T> in NestSerialization, effectively creating a recursive type:
Serialize<T> uses NestSerialization<T> uses Serialize<T>, and so on:
typeSerialize<T>=NestSerialization<Remove<T,Function>>;
typeSerialize<T>=NestSerialization<Remove<T,Function>>;
TypeScript 可以在一定程度上处理类型递归。在本例中,它可以看到 中确实存在一个跳出类型递归的条件NestSerialization。
TypeScript can handle type recursion to a certain degree. In this case, it can see that there is literally a condition to break out of type recursion in NestSerialization.
这就是序列化类型!现在来看看函数的实现,奇怪的是,它是 JavaScript 中类型声明的直接翻译。我们检查每个属性是否是对象。如果是,我们serialize再次调用。如果不是,我们仅当它不是函数时才传递该属性:
And that’s serialization type! Now for the implementation of the function, which is curiously a straight translation of our type declaration in JavaScript. We check for every property if it’s an object. If so, we call serialize again. If not, we carry over the property only if it isn’t a function:
classSerializer{constructor(){}serialize<T>(obj:T):Serialize<T>{constret:Record<string,any>={};for(letkinobj){if(typeofobj[k]==="object"){ret[k]=this.serialize(obj[k]);}elseif(typeofobj[k]!=="function"){ret[k]=obj[k];}}returnretasSerialize<T>;}}
classSerializer{constructor(){}serialize<T>(obj:T):Serialize<T>{constret:Record<string,any>={};for(letkinobj){if(typeofobj[k]==="object"){ret[k]=this.serialize(obj[k]);}elseif(typeofobj[k]!=="function"){ret[k]=obj[k];}}returnretasSerialize<T>;}}
请注意,由于我们在 中生成一个新对象serialize,因此我们从非常宽泛的 开始Record<string, any>,这允许我们将任何字符串属性键设置为基本上任何内容,并在最后断言我们创建了一个适合我们的返回类型的对象。这种模式在创建新对象时很常见,但最终要求您 100% 确定您所做的一切都正确无误。请
广泛测试此功能。
Note that since we are generating a new object within serialize, we start out with a very broad Record<string, any>, which allows us to set any string property key to basically anything, and assert at the end that we created an object that fits our return type. This pattern is common when you create new objects, but it ultimately requires you to be 100% sure that you did everything right. Please test this function
extensively.
完成第一个实现后,我们可以创建一个新的类型对象Person并将其传递给我们新生成的序列化器:
With the first implementation done, we can create a new object of type Person and pass it to our newly generated serializer:
constperson:Person={name:"Stefan",age:40,profession:{title:"Software Developer",level:5,printProfession(){console.log(`${this.title}, Level${this.level}`);},},hello(){return`Hello${this.name}`;},};constserializer=newSerializer();constserializedPerson=serializer.serialize(person);console.log(serializedPerson);
constperson:Person={name:"Stefan",age:40,profession:{title:"Software Developer",level:5,printProfession(){console.log(`${this.title}, Level${this.level}`);},},hello(){return`Hello${this.name}`;},};constserializer=newSerializer();constserializedPerson=serializer.serialize(person);console.log(serializedPerson);
结果正如预期的那样: 的类型serializedPerson缺少有关方法和函数的所有信息。 如果我们记录serializedPerson,我们还会看到所有方法和函数都消失了。 类型与实现结果相匹配:
The result is as expected: the type of serializedPerson lacks all information on methods and functions. And if we log serializedPerson, we also see that all methods and functions are gone. The type matches the implementation result:
[日志]: {
“名称”:“Stefan”,
“年龄”:40,
“职业”: {
“title”:“软件开发人员”,
“级别”:5
}
}[LOG]: {
"name": "Stefan",
"age": 40,
"profession": {
"title": "Software Developer",
"level": 5
}
}
但我们还没有完成。序列化器有一个特殊功能。对象可以实现一个serialize方法,如果它们实现了,序列化器将采用此方法的输出,而不是自己序列化对象。让我们扩展类型Person以具有一种serialize方法:
But we are not done yet. The serializer has a special feature. Objects can implement a serialize method, and if they do, the serializer takes the output of this method instead of serializing the object on its own. Let’s extend the Person type to feature a serialize method:
typePerson={name:string;age:number;profession:{title:string;level:number;printProfession:()=>void;};hello:()=>string;serialize:()=>string;};constperson:Person={name:"Stefan",age:40,profession:{title:"Software Developer",level:5,printProfession(){console.log(`${this.title}, Level${this.level}`);},},hello(){return`Hello${this.name}`;},serialize(){return`${this.name}:${this.profession.title}L${this.profession.level}`;},};
typePerson={name:string;age:number;profession:{title:string;level:number;printProfession:()=>void;};hello:()=>string;serialize:()=>string;};constperson:Person={name:"Stefan",age:40,profession:{title:"Software Developer",level:5,printProfession(){console.log(`${this.title}, Level${this.level}`);},},hello(){return`Hello${this.name}`;},serialize(){return`${this.name}:${this.profession.title}L${this.profession.level}`;},};
我们需要调整Serialize<T>类型。在运行之前NestSerialization,我们会在条件类型中检查对象是否实现了serialize方法。我们通过询问是否T是包含方法的类型的子类型来做到这一点serialize。如果是,我们需要获取返回类型,因为这是序列化的结果。
We need to adapt the Serialize<T> type. Before running NestSerialization, we check in a conditional type if the object implements a serialize method. We do so by asking if T is a subtype of a type that contains a serialize method. If so, we need to get to the return type, because that’s the result of serialization.
这就是infer关键字发挥作用的地方。它允许我们从条件中获取类型并将其用作分支中的类型参数true。我们告诉 TypeScript,如果此条件为真,则获取您在那里找到的类型并将其提供给我们:
This is where the infer keyword comes into play. It allows us to take a type from a condition and use it as a type parameter in the true branch. We tell TypeScript, if this condition is true, take the type that you found there and make it available to us:
typeSerialize<T>=Textends{serialize():inferR}?R:NestSerialization<Remove<T,Function>>;
typeSerialize<T>=Textends{serialize():inferR}?R:NestSerialization<Remove<T,Function>>;
认为R是any首先。如果我们进行检查Person,{ serialize(): any }我们就会跳转到true分支,因为它Person具有serialize函数,使其成为有效的子类型。但是any是广泛的,并且我们对 位置处的特定类型感兴趣any。infer关键字可以选择该确切类型。所以Serialize<T>现在读作:
Think of R as being any at first. If we check Person against { serialize(): any } we hop into the true branch, as Person has a serialize function, making it a valid sub-type. But any is broad, and we are interested in the specific type at the position of any. The infer keyword can pick that exact type. So Serialize<T> now reads:
如果T包含serialize方法,则获取其返回类型并返回它。
If T contains a serialize method, get its return type and return it.
否则,通过深度删除所有类型的属性来开始序列化Function。
Otherwise, start serialization by deeply removing all properties that are of type Function.
我们希望在 JavaScript 实现中也反映该类型的行为。我们进行了一些类型检查(检查是否serialize可用以及是否是函数)并最终调用它。TypeScript 要求我们明确使用类型保护,以确保该函数存在:
We want to mirror that type’s behavior in our JavaScript implementation as well. We do a couple of type-checks (checking if serialize is available and if it’s a function) and ultimately call it. TypeScript requires us to be explicit with type guards, to be absolutely sure that this function exists:
classSerializer{constructor(){}serialize<T>(obj:T):Serialize<T>{if(// is an objecttypeofobj==="object"&&// not nullobj&&// serialize is available"serialize"inobj&&// and a functiontypeofobj.serialize==="function"){returnobj.serialize();}constret:Record<string,any>={};for(letkinobj){if(typeofobj[k]==="object"){ret[k]=this.serialize(obj[k]);}elseif(typeofobj[k]!=="function"){ret[k]=obj[k];}}returnretasSerialize<T>;}}
classSerializer{constructor(){}serialize<T>(obj:T):Serialize<T>{if(// is an objecttypeofobj==="object"&&// not nullobj&&// serialize is available"serialize"inobj&&// and a functiontypeofobj.serialize==="function"){returnobj.serialize();}constret:Record<string,any>={};for(letkinobj){if(typeofobj[k]==="object"){ret[k]=this.serialize(obj[k]);}elseif(typeofobj[k]!=="function"){ret[k]=obj[k];}}returnretasSerialize<T>;}}
经过这样的改变, 的类型serializedPerson就变为string,结果也符合预期:
With this change, the type of serializedPerson is string, and the result is as expected:
[日志]:“Stefan:软件开发人员 L5”
[LOG]: "Stefan: Software Developer L5"
这个强大的工具对对象生成有很大帮助。我们使用声明性元语言(即 TypeScript 的类型系统)创建类型,最终看到用 JavaScript 命令式编写的相同过程,这真是太棒了。
This powerful tool helps greatly with object generation. And there’s beauty in the fact that we create a type using a declarative metalanguage that is TypeScript’s type system, to ultimately see the same process imperatively written in JavaScript.
在 TypeScript 的类型系统中,每个值也是一种类型。我们称它们为文字类型,与其他文字类型联合使用,您可以定义一种类型,该类型非常清楚它可以接受哪些值。让我们以子集string为例。您可以准确定义哪些字符串应该成为您的集合的一部分,并排除大量错误。另一端将是整个字符串集。
In TypeScript’s type system, every value is also a type. We call them literal types, and in union with other literal types, you can define a type that is very clear about which values it can accept. Let’s take subsets of string as an example. You can define exactly which strings should be part of your set and rule out a ton of errors. The other end of the spectrum would be the entire set of strings again.
但是如果两者之间存在某种关系呢?如果我们可以定义检查某些字符串模式是否可用的类型,并让其余部分更加灵活,会怎么样?字符串模板文字类型正是这样做的。它们允许我们定义预定义字符串某些部分的类型;其余部分对于各种用途都是开放和灵活的。
But what if there is something between? What if we can define types that check if certain string patterns are available, and let the rest be more flexible? String template literal types do exactly that. They allow us to define types where certain parts of a string are predefined; the rest is open and flexible for a variety of uses.
但更重要的是,结合条件类型,可以将字符串拆分成小块,并将相同的部分重新用于新类型。这是一个非常强大的工具,特别是当你考虑到 JavaScript 中有多少代码依赖于字符串中的模式时。
But even more, in conjunction with conditional types, it’s possible to split strings into bits and pieces and reuse the same bits for new types. This is an incredibly powerful tool, especially if you think about how much code in JavaScript relies on patterns within strings.
在本章中,我们将介绍字符串模板字面量类型的各种用例。从遵循简单的字符串模式到根据格式字符串提取参数和类型,您将看到将字符串解析为类型的强大功能。
In this chapter, we look at a variety of use cases for string template literal types. From following simple string patterns to extracting parameters and types based on format strings, you will see the enabling power of parsing strings as types.
但我们坚持实事求是。您在此处看到的所有内容均来自真实示例。使用字符串模板文字类型可以实现的功能似乎无穷无尽。人们通过编写拼写检查器或实现SQL 解析器将字符串模板文字类型的使用推向极致;使用这项令人惊叹的功能似乎可以实现无限的功能。
But we keep it real. Everything you see here comes from real-world examples. What you can accomplish with string template literal types seems endless. People push the usage of string template literal types to the extreme by writing spell checkers or implementing SQL parsers; there seems to be no limit to what you can do with this mind-blowing feature.
在 JavaScript 事件系统中,通常使用某种前缀来表示特定字符串是事件。通常,事件或事件处理程序字符串以 开头on,但根据实现的不同,这可能会有所不同。
It’s common in JavaScript event systems to have some sort of prefix that indicates a particular string is an event. Usually, event or event handler strings start with on, but depending on the implementation, this can be different.
您想创建自己的事件系统并遵守此约定。使用 TypeScript 的字符串类型,可以接受所有可能的字符串或子集到字符串文字类型的联合类型。虽然一个太宽泛,但另一个对于我们的需求来说不够灵活。我们不想预先定义所有可能的事件名称;我们希望遵循一种模式。
You want to create your own event system and want to honor this convention. With TypeScript’s string types it is possible to either accept all possible strings or subset to a union type of string literal types. While one is too broad, the other one is not flexible enough for our needs. We don’t want to define every possible event name up front; we want to adhere to a pattern.
幸运的是,我们正在寻找一种称为字符串模板文字类型或简称为模板文字类型的类型。模板文字类型允许我们定义字符串文字,但保留某些部分的灵活性。
Thankfully, a type called string template literal type or just template literal type is exactly what we are looking for. Template literal types allow us to define string literals but leave certain parts flexible.
例如,接受以 开头的所有字符串的类型on可能如下所示:
For example, a type that accepts all strings that start with on could look like this:
typeEventName=`on${string}`;
typeEventName=`on${string}`;
从语法上讲,模板字面量类型借用了 JavaScript 的模板字符串。它们以反引号开头和结尾,后跟任意字符串。
Syntactically, template literal types borrow from JavaScript’s template strings. They start and end with a backtick, followed by any string.
使用特定的语法${}允许向字符串添加 JavaScript 表达式,如变量、函数调用等:
Using the specific syntax ${} allows adding JavaScript expressions, like variables, function calls, and the like to strings:
functiongreet(name:string){return`Hi,${name}!`;}greet("Stefan");// "Hi, Stefan!"
functiongreet(name:string){return`Hi,${name}!`;}greet("Stefan");// "Hi, Stefan!"
TypeScript 中的模板文字类型非常相似。它们允许我们以类型的形式添加一组值,而不是 JavaScript 表达式。定义 HTML 中所有可用标题元素的字符串表示形式的类型可能如下所示:
Template literal types in TypeScript are very similar. Instead of JavaScript expressions, they allow us to add a set of values in the form of types. A type defining the string representation of all available heading elements in HTML could look like this:
typeLevels=1|2|3|4|5|6;// resolves to "H1" | "H2" | "H3" | "H4" | "H5" | "H6"typeHeadings=`H${Levels}`;
typeLevels=1|2|3|4|5|6;// resolves to "H1" | "H2" | "H3" | "H4" | "H5" | "H6"typeHeadings=`H${Levels}`;
Levels是 的子集number,Headings读作“以 H 开头,后跟与 兼容的值Levels”。您不能将每种类型都放在这里,只能将具有字符串表示的类型放在这里。
Levels is a subset of number, and Headings reads as “starts with H, followed by a value compatible with Levels.” You can’t put every type in here, only ones that have a string representation.
让我们回顾一下EventName:
Let’s go back to EventName:
typeEventName=`on${string}`;
typeEventName=`on${string}`;
定义如下,EventName读起来就像“以 开头"on",后跟任意字符串”。这包括空字符串。让我们使用EventName来创建一个简单的事件系统。在第一步中,我们只想收集回调函数。
Defined like this, EventName reads like “starts with "on", followed by any string.” This includes the empty string. Let’s use EventName to create a simple event system. In the first step, we only want to collect callback functions.
为此,我们定义一个Callback类型,该类型是具有一个参数的函数类型: an EventObject。这EventObject是一个包含事件信息值的泛型类型:
For that, we define a Callback type that is a function type with one parameter: an EventObject. The EventObject is a generic type that contains the value with the event information:
typeEventObject<T>={val:T;};typeCallback<T=any>=(ev:EventObject<T>)=>void;
typeEventObject<T>={val:T;};typeCallback<T=any>=(ev:EventObject<T>)=>void;
此外,我们需要一个类型来存储所有已注册的事件回调Events:
Furthermore, we need a type to store all registered event callbacks, Events:
typeEvents={[x:EventName]:Callback[]|undefined;};
typeEvents={[x:EventName]:Callback[]|undefined;};
我们使用EventName索引访问,因为它是 的有效子类型string。每个索引指向一个回调数组。定义好类型后,我们设置一个EventSystem类:
We use EventName as index access as it is a valid subtype of string. Each index points to an array of callbacks. With our types defined, we set up an EventSystem class:
classEventSystem{events:Events;constructor(){this.events={};}defineEventHandler(ev:EventName,cb:Callback):void{this.events[ev]=this.events[ev]??[];this.events[ev]?.push(cb);}trigger(ev:EventName,value:any){letcallbacks=this.events[ev];if(callbacks){callbacks.forEach((cb)=>{cb({val:value});});}}}
classEventSystem{events:Events;constructor(){this.events={};}defineEventHandler(ev:EventName,cb:Callback):void{this.events[ev]=this.events[ev]??[];this.events[ev]?.push(cb);}trigger(ev:EventName,value:any){letcallbacks=this.events[ev];if(callbacks){callbacks.forEach((cb)=>{cb({val:value});});}}}
构造函数创建一个新的事件存储,并defineEventHandler采用EventNameandCallback并将它们存储在所述事件存储中。此外,trigger采用EventNameand,如果已注册回调,则使用 执行每个已注册的回调EventObject。
The constructor creates a new events storage, and defineEventHandler takes an EventName and Callback and stores them in said events storage. Also, trigger takes an EventName and, if callbacks are registered, executes every registered callback with an EventObject.
第一步已经完成。现在我们在定义事件时有了类型安全:
The first step is done. We now have type safety when defining events:
constsystem=newEventSystem();system.defineEventHandler("click",()=>{});// ^ Argument of type '"click"' is not assignable to parameter//. of type '`on${string}`'.(2345)system.defineEventHandler("onClick",()=>{});system.defineEventHandler("onchange",()=>{});
constsystem=newEventSystem();system.defineEventHandler("click",()=>{});// ^ Argument of type '"click"' is not assignable to parameter//. of type '`on${string}`'.(2345)system.defineEventHandler("onClick",()=>{});system.defineEventHandler("onchange",()=>{});
在方案 6.2中,我们将研究如何使用字符串操作类型和键重新映射来增强我们的系统。
In Recipe 6.2 we will look at how we can use string manipulation types and key remapping to enhance our system.
使用键重映射来创建新的字符串属性键。使用字符串操作类型来为观察器函数提供正确的驼峰式大小写。
Use key remapping to create new string property keys. Use string manipulation types to have proper camel casing for watcher functions.
方案 6.1中的事件系统正在成型。我们能够注册事件处理程序并触发事件。现在我们要添加监视功能。我们的想法是使用方法来扩展有效对象,以注册每次属性更改时执行的回调。例如,当我们定义一个person对象时,我们应该能够监听onAgeChanged和onNameChanged事件:
Our event system from Recipe 6.1 is taking shape. We are able to register event handlers and trigger events. Now we want to add watch functionality. The idea is to extend valid objects with methods for registering callbacks that are executed every time a property changes. For example, when we define a person object, we should be able to listen to onAgeChanged and onNameChanged events:
letperson={name:"Stefan",age:40,};constwatchedPerson=system.watch(person);watchedPerson.onAgeChanged((ev)=>{console.log(ev.val,"changed!!");});watchedPerson.age=41;// triggers callbacks
letperson={name:"Stefan",age:40,};constwatchedPerson=system.watch(person);watchedPerson.onAgeChanged((ev)=>{console.log(ev.val,"changed!!");});watchedPerson.age=41;// triggers callbacks
因此对于每个属性,都会有一个以 开头on、以 结尾Changed并接受带有事件对象参数的回调函数的方法。
So for each property, there will be a method that starts with on, ends with Changed, and accepts callback functions with event object parameters.
为了定义新的事件处理程序方法,我们创建了一个名为的辅助类型WatchedObject<T>,在其中添加了定制方法:
To define the new event handler methods, we create a helper type called WatchedObject<T>, where we add bespoke methods:
typeWatchedObject<T>={[Kinstring&keyofTas`on${K}Changed`]:(ev:Callback<T[K]>)=>void;};
typeWatchedObject<T>={[Kinstring&keyofTas`on${K}Changed`]:(ev:Callback<T[K]>)=>void;};
有很多内容需要解开。让我们一步一步来:
There’s a lot to unpack. Let’s go through it step by step:
我们通过迭代 中的所有键来定义映射类型T。由于我们只关心string属性键,因此我们使用交集string & keyof T来摆脱潜在的符号或数字。
We define a mapped type by iterating over all keys from T. Since we care only about string property keys, we use the intersection string & keyof T to get rid of potential symbols or numbers.
接下来,我们将此键重新映射到一个新字符串,该字符串由字符串模板文字类型定义。它以 开头,然后从映射过程中on获取键,并附加。KChanged
Next, we remap this key to a new string, defined by a string template literal type. It starts with on, then takes the key K from our mapping process, and appends Changed.
属性 key 指向一个接受回调的函数。回调本身有一个事件对象作为参数,通过正确替换其泛型,我们可以确保此事件对象包含我们监视的对象的原始类型。这意味着当我们调用时onAgeChanged,事件对象实际上将包含一个number。
The property key points to a function that accepts a callback. The callback itself has an event object as an argument, and by correctly substituting its generics, we can make sure this event object contains the original type of our watched object. This means when we call onAgeChanged, the event object will actually contain a number.
这已经很棒了,但缺少重要的细节。当我们像这样使用WatchedObjecton时,所有生成的事件处理程序方法在 后都缺少一个大写字符。为了解决这个问题,我们可以使用内置的字符串操作类型之一将
字符串类型大写:personon
This is already fantastic but lacks significant detail. When we use WatchedObject on person like that, all generated event handler methods lack an uppercase character after on. To solve this, we can use one of the built-in string manipulation types to
capitalize string types:
typeWatchedObject<T>={[Kinstring&keyofTas`on${Capitalize<K>}Changed`]:(ev:Callback<T[K]>)=>void;};
typeWatchedObject<T>={[Kinstring&keyofTas`on${Capitalize<K>}Changed`]:(ev:Callback<T[K]>)=>void;};
旁边还有,,,Capitalize和。如果我们将鼠标悬停在上,我们可以看到生成的类型是什么样的:LowercaseUppercaseUncapitalizeWatchedObject<typeof person>
Next to Capitalize, Lowercase, Uppercase, and Uncapitalize are also available. If we hover over WatchedObject<typeof person>, we can see what the generated type looks like:
typeWatchedPerson={onNameChanged:(ev:Callback<string>)=>void;onAgeChanged:(ev:Callback<number>)=>void;};
typeWatchedPerson={onNameChanged:(ev:Callback<string>)=>void;onAgeChanged:(ev:Callback<number>)=>void;};
设置好类型后,我们就开始实现。首先,我们创建两个辅助函数:
With our types set up, we start with the implementation. First, we create two helper functions:
functioncapitalize(inp:string){returninp.charAt(0).toUpperCase()+inp.slice(1);}functionhandlerName(name:string):EventName{return`on${capitalize(name)}Changed`asEventName;}
functioncapitalize(inp:string){returninp.charAt(0).toUpperCase()+inp.slice(1);}functionhandlerName(name:string):EventName{return`on${capitalize(name)}Changed`asEventName;}
我们需要这两个辅助函数来模仿 TypeScript 重新映射和操作字符串的行为。capitalize将字符串的首字母更改为大写字母,并handlerName为其添加前缀和后缀。handlerName我们需要一个小的类型断言来向 TypeScript 发出类型已更改的信号。在 JavaScript 中,我们可以通过多种方式转换字符串,但 TypeScript 无法确定这会导致大写版本。
We need both helper functions to mimic TypeScript’s behavior of remapping and manipulating strings. capitalize changes the first letter of a string to its uppercase equivalent, and handlerName adds a prefix and suffix to it. With handlerName we need a little type assertion to signal TypeScript that the type has changed. With the many ways we can transform strings in JavaScript, TypeScript can’t figure out that this will result in a capitalized version.
接下来,我们在事件系统中实现该watch功能。我们创建一个通用函数,它接受任何对象并返回一个包含原始属性和观察者属性的对象。
Next, we implement the watch functionality in the event system. We create a generic function that accepts any object and returns an object that contains both the original properties and the watcher properties.
为了成功实现在属性改变时触发事件处理程序,我们使用Proxy对象来拦截get并set调用:
To successfully implement triggering of event handlers on property change, we use Proxy objects to intercept get and set calls:
classEventSystem{// cut for brevitywatch<Textendsobject>(obj:T):T&WatchedObject<T>{constself=this;returnnewProxy(obj,{get(target,property){// (1)if(typeofproperty==="string"&&property.startsWith("on")&&property.endsWith("Changed")){// (2)return(cb:Callback)=>{self.defineEventHandler(propertyasEventName,cb);};}// (3)returntarget[propertyaskeyofT];},// set to be done ...})asT&WatchedObject<T>;}}
classEventSystem{// cut for brevitywatch<Textendsobject>(obj:T):T&WatchedObject<T>{constself=this;returnnewProxy(obj,{get(target,property){// (1)if(typeofproperty==="string"&&property.startsWith("on")&&property.endsWith("Changed")){// (2)return(cb:Callback)=>{self.defineEventHandler(propertyasEventName,cb);};}// (3)returntarget[propertyaskeyofT];},// set to be done ...})asT&WatchedObject<T>;}}
get每当我们访问以下属性时,我们想要拦截的
调用WatchedObject<T>:
The get calls we want to intercept are whenever we access the properties of
WatchedObject<T>:
它们以 开始on,也以 结束Changed。
They start with on and end with Changed.
如果是这种情况,我们将返回一个接受回调的函数。该函数本身通过 将回调添加到事件存储中defineEventHandler。
If that’s the case, we return a function that accepts callbacks. The function itself adds callbacks to the event storage via defineEventHandler.
在所有其他情况下,我们都会进行常规的财产访问。
In all other cases we do regular property access.
现在,每次我们设置原始对象的值时,我们都希望触发存储的事件。这就是我们修改所有set调用的原因:
Now, every time we set a value of the original object, we want to trigger stored events. This is why we modify all set calls:
classEventSystem{// ... cut for brevitywatch<Textendsobject>(obj:T):T&WatchedObject<T>{constself=this;returnnewProxy(obj,{// get from above ...set(target,property,value){if(propertyintarget&&typeofproperty==="string"){// (1)target[propertyaskeyofT]=value;// (2)self.trigger(handlerName(property),value);returntrue;}returnfalse;},})asT&WatchedObject<T>;}}
classEventSystem{// ... cut for brevitywatch<Textendsobject>(obj:T):T&WatchedObject<T>{constself=this;returnnewProxy(obj,{// get from above ...set(target,property,value){if(propertyintarget&&typeofproperty==="string"){// (1)target[propertyaskeyofT]=value;// (2)self.trigger(handlerName(property),value);returntrue;}returnfalse;},})asT&WatchedObject<T>;}}
流程如下:
The process is as follows:
设置值。无论如何,我们都需要更新对象。
Set the value. We need to update the object anyway.
调用该trigger函数执行所有已注册的回调。
Call the trigger function to execute all registered callbacks.
请注意,我们需要几个类型断言来推动 TypeScript 朝着正确的方向发展。毕竟,我们正在创建新对象。
Please note that we need a couple of type assertions to nudge TypeScript in the right direction. We are creating new objects, after all.
就这样!尝试从头开始的示例,看看你的事件系统如何运行:
And that’s it! Try the example from the beginning to see your event system in action:
letperson={name:"Stefan",age:40,};constwatchedPerson=system.watch(person);watchedPerson.onAgeChanged((ev)=>{console.log(ev.val,"changed!!");});watchedPerson.age=41;// logs "41 changed!!"
letperson={name:"Stefan",age:40,};constwatchedPerson=system.watch(person);watchedPerson.onAgeChanged((ev)=>{console.log(ev.val,"changed!!");});watchedPerson.age=41;// logs "41 changed!!"
字符串模板文字类型以及字符串操作类型和键重映射使我们能够动态创建新对象的类型。这些强大的工具使高级 JavaScript 对象创建的使用更加可靠。
String template literal types along with string manipulation types and key remapping allow us to create types for new objects on the fly. These powerful tools make the use of advanced JavaScript object creation more robust.
创建一个条件类型,从字符串模板文字类型推断占位符名称 。
Create a conditional type that infers the placeholder name from a string template literal type.
您的应用程序有一种定义格式字符串的方法,即使用花括号定义占位符。第二个参数采用带有替换项的对象,因此对于格式字符串中定义的每个占位符,都有一个具有相应值的属性键:
Your application has a way of defining format strings by defining placeholders with curly braces. A second parameter takes an object with substitutions, so for each placeholder defined in the format string, there is one property key with the respective value:
format("Hello {world}. My name is {you}.",{world:"World",you:"Stefan",});
format("Hello {world}. My name is {you}.",{world:"World",you:"Stefan",});
让我们为这个函数创建类型,以确保您的用户不会忘记添加所需的属性。作为第一步,我们用一些非常广泛的类型定义函数接口。格式字符串的类型为string,格式化参数是Record键string和任意值。我们首先关注类型;函数主体的实现稍后再讨论:
Let’s create typings for this function, where we make sure that your users don’t forget to add the required properties. As a first step, we define the function interface with some very broad types. The format string is of type string, and the formatting parameters are in a Record of string keys and literally any value. We focus on the types first; the function body’s implementation comes later:
functionformat(fmtString:string,params:Record<string,any>):string{throw"unimplemented";}
functionformat(fmtString:string,params:Record<string,any>):string{throw"unimplemented";}
下一步,我们想通过添加泛型将函数参数锁定为具体值或文字类型。我们将 的类型更改fmtString为泛型类型T,它是 的子类型string。这允许我们仍然可以将字符串传递给函数,但是当我们传递文字字符串时,我们可以分析文字类型并寻找模式(有关更多详细信息,请参阅方案 4.3 ):
As a next step, we want to lock function arguments to concrete values or literal types by adding generics. We change the type of fmtString to be of a generic type T, which is a subtype of string. This allows us to still pass strings to the function, but the moment we pass a literal string, we can analyze the literal type and look for patterns (see Recipe 4.3 for more details):
functionformat<Textendsstring>(fmtString:T,params:Record<string,any>):string{throw"unimplemented";}
functionformat<Textendsstring>(fmtString:T,params:Record<string,any>):string{throw"unimplemented";}
现在我们锁定了T,我们可以将其作为类型参数传递给泛型类型FormatKeys。这是一个条件类型,它将扫描格式字符串中的花括号:
Now that we locked in T, we can pass it as a type parameter to a generic type FormatKeys. This is a conditional type that will scan our format string for curly braces:
typeFormatKeys<Textendsstring>=Textends`${string}{${string}}${string}`?T:never;
typeFormatKeys<Textendsstring>=Textends`${string}{${string}}${string}`?T:never;
在这里,我们检查格式字符串:
Here, we check if the format string:
以字符串开头;也可以是空字符串
Starts with a string; this can also be an empty string
包含一个{,后跟任意字符串,后跟一个}
Contains a {, followed by any string, followed by a }
后面跟着任意字符串
Is followed again by any string
这实际上意味着我们检查格式字符串中是否只有一个占位符。如果是,我们返回整个格式字符串,如果不是,我们返回never:
This effectively means that we check if there is exactly one placeholder in the format string. If so, we return the entire format string, and if not, we return never:
typeA=FormatKeys<"Hello {world}">;// "Hello {world}"typeB=FormatKeys<"Hello">;// never
typeA=FormatKeys<"Hello {world}">;// "Hello {world}"typeB=FormatKeys<"Hello">;// never
FormatKeys可以告诉我们传入的字符串是否是格式字符串,但实际上我们对格式字符串的特定部分更感兴趣:花括号之间的部分。使用 TypeScript 的infer关键字,我们可以告诉 TypeScript,如果格式字符串与此模式匹配,则获取在花括号之间找到的任何文字类型并将其放入类型变量中:
FormatKeys can tell us if the strings we pass in are format strings or not, but we are actually much more interested in a specific part of the format string: the piece between the curly braces. Using TypeScript’s infer keyword, we can tell TypeScript that, if the format string matches this pattern, then grab whatever literal type you find between the curly braces and put it in a type variable:
typeFormatKeys<Textendsstring>=Textends`${string}{${inferKey}}${string}`?Key:never;
typeFormatKeys<Textendsstring>=Textends`${string}{${inferKey}}${string}`?Key:never;
这样,我们可以提取子字符串并根据需要重新使用它们:
That way, we can extract substrings and reuse them for our needs:
typeA=FormatKeys<"Hello {world}">;// "world"typeB=FormatKeys<"Hello">;// never
typeA=FormatKeys<"Hello {world}">;// "world"typeB=FormatKeys<"Hello">;// never
太棒了!我们提取了第一个占位符名称。现在开始其余部分。由于后面可能还有占位符,我们将第一个占位符后面的所有内容存储在名为 的类型变量中Rest。此条件将始终为真,因为它要么Rest是空字符串,要么包含我们可以再次分析的实际字符串。
Fantastic! We extracted the first placeholder name. Now on to the rest. Since there might be placeholders following, we take everything after the first placeholder and store it in a type variable called Rest. This condition will be always true, because either Rest is the empty string or it contains an actual string that we can analyze again.
我们在分支调用中采用联合类型的Restand :trueFormatKeys<Rest>Key
We take the Rest and in the true branch call FormatKeys<Rest> in a union type of Key:
typeFormatKeys<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?Key|FormatKeys<Rest>:never;
typeFormatKeys<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?Key|FormatKeys<Rest>:never;
这是一个递归条件类型。结果将是占位符的并集,我们可以将其用作格式化对象的键:
This is a recursive conditional type. The result will be a union of placeholders, which we can use as keys for the formatting object:
typeA=FormatKeys<"Hello {world}">;// "world"typeB=FormatKeys<"Hello {world}. I'm {you}.">;// "world" | "you"typeC=FormatKeys<"Hello">;// never
typeA=FormatKeys<"Hello {world}">;// "world"typeB=FormatKeys<"Hello {world}. I'm {you}.">;// "world" | "you"typeC=FormatKeys<"Hello">;// never
现在是时候连接了FormatKeys。由于我们已经锁定了T,我们可以将其作为参数传递给FormatKeys,我们可以将其用作的参数Record:
Now it’s time to wire up FormatKeys. Since we already locked in T, we can pass it as an argument to FormatKeys, which we can use as an argument for Record:
functionformat<Textendsstring>(fmtString:T,params:Record<FormatKeys<T>,any>):string{throw"unimplemented";}
functionformat<Textendsstring>(fmtString:T,params:Record<FormatKeys<T>,any>):string{throw"unimplemented";}
这样,我们的类型就全部准备好了。开始实现!实现与我们定义类型的方式完美地相反。我们检查所有键,params并将花括号内的所有出现替换为相应的值:
And with that, our typings are all ready. On to the implementation! The implementation is beautifully inverted to how we defined our types. We go over all keys from params and replace all occurrences within curly braces with the respective value:
functionformat<Textendsstring>(fmtString:T,params:Record<FormatKeys<T>,any>):string{letret:string=fmtString;for(letkinparams){ret=ret.replaceAll(`{${k}}`,params[kaskeyoftypeofparams]);}returnret;}
functionformat<Textendsstring>(fmtString:T,params:Record<FormatKeys<T>,any>):string{letret:string=fmtString;for(letkinparams){ret=ret.replaceAll(`{${k}}`,params[kaskeyoftypeofparams]);}returnret;}
请注意两种特殊的类型:
Notice two particular typings:
我们需要ret用 进行注释string。fmtString是 的T子类型string;因此ret也是T。这意味着我们无法更改值,因为 的类型T会发生变化。将其注释为更广泛的string类型有助于我们修改ret。
We need to annotate ret with string. fmtString is with T, a subtype of string; thus ret would also be T. This would mean we couldn’t change values because the type of T would change. Annotating it to a broader string type helps us modify ret.
我们还需要断言对象键k实际上是的键params。这是一个不太好的解决方法,这是由于 TypeScript 的一些故障安全机制造成的。有关此主题的更多信息,请参阅方案 9.1。
We also need to assert that the object key k is actually a key of params. This is an unfortunate workaround that is due to some fail-safe mechanisms of TypeScript. Read more on this topic in Recipe 9.1.
利用9.1 节中的信息,我们可以重新定义format以摆脱一些类型断言,从而得到函数的最终版本format:
With the information from Recipe 9.1, we can redefine format to get rid of some type assertions to reach our final version of the format function:
functionformat<Textendsstring,KextendsRecord<FormatKeys<T>,any>>(fmtString:T,params:K):string{letret:string=fmtString;for(letkinparams){ret=ret.replaceAll(`{${k}}`,params[k]);}returnret;}
functionformat<Textendsstring,KextendsRecord<FormatKeys<T>,any>>(fmtString:T,params:K):string{letret:string=fmtString;for(letkinparams){ret=ret.replaceAll(`{${k}}`,params[k]);}returnret;}
能够拆分字符串并提取属性键的功能非常强大。全世界的 TypeScript 开发人员都使用此模式来增强类型,例如,用于Express等 Web 服务器。我们将看到更多有关如何使用此工具获得更好类型的示例。
Being able to split strings and extract property keys is extremely powerful. TypeScript developers all over the world use this pattern to strengthen types, for example, for web servers like Express. We will see more examples of how we can use this tool to get better types.
您想要扩展配方 6.3中的格式化函数,使其能够定义占位符的类型。
You want to extend the formatting function from Recipe 6.3 with the ability to define types for your placeholders.
创建嵌套的条件类型并使用类型映射查找类型。
Create a nested conditional type and look up types with a type map.
让我们扩展上一课中的示例。我们现在不仅想要知道所有占位符,还想要能够用占位符定义一组特定的类型。类型应该是可选的,在占位符名称后用冒号表示,并且是 JavaScript 的原始类型之一。当我们传入错误类型的值时,我们预计会出现类型错误:
Let’s extend the example from the previous lesson. We now want to not only know all placeholders but also be able to define a certain set of types with the placeholders. Types should be optional, be indicated with a colon after the placeholder name, and be one of JavaScript’s primitive types. We expect to get type errors when we pass in a value that is of the wrong type:
format("Hello {world:string}. I'm {you}, {age:number} years old.",{world:"World",age:40,you:"Stefan",});
format("Hello {world:string}. I'm {you}, {age:number} years old.",{world:"World",age:40,you:"Stefan",});
作为参考,我们来看看方案 6.3中的原始实现:
For reference, let’s look at the original implementation from Recipe 6.3:
typeFormatKeys<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?Key|FormatKeys<Rest>:never;functionformat<Textendsstring>(fmtString:T,params:Record<FormatKeys<T>,any>):string{letret:string=fmtString;for(letkinparams){ret=ret.replace(`{${k}}`,params[kaskeyoftypeofparams]);}returnret;}
typeFormatKeys<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?Key|FormatKeys<Rest>:never;functionformat<Textendsstring>(fmtString:T,params:Record<FormatKeys<T>,any>):string{letret:string=fmtString;for(letkinparams){ret=ret.replace(`{${k}}`,params[kaskeyoftypeofparams]);}returnret;}
为了实现这一点,我们需要做两件事:
To achieve this, we need to do two things:
将类型更改params为Record<FormatKeys<T>, any>与每个属性键关联的适当类型的实际对象类型。
Change the type of params from Record<FormatKeys<T>, any> to an actual object type that has proper types associated with each property key.
调整其中的字符串模板文字类型FormatKeys以便能够提取原始 JavaScript 类型。
Adapt the string template literal type within FormatKeys to be able to extract primitive JavaScript types.
第一步,我们引入一种名为 的新类型FormatObj<T>。它的工作原理与之前相同FormatKeys,但它不是简单地返回字符串键,而是将相同的键映射到新的对象类型。这要求我们使用交集类型而不是联合类型来链接递归(我们在每次递归中添加更多属性),并将中断条件从 更改为never。{}如果我们与 进行交集never,则整个返回类型将变为never。这样,我们就不会向返回类型添加任何新属性:
For the first step, we introduce a new type called FormatObj<T>. It works just as FormatKeys did, but instead of simply returning string keys, it maps out the same keys to a new object type. This requires us to chain the recursion using intersection types instead of a union type (we add more properties with each recursion) and to change the breaking condition from never to {}. If we did an intersection with never, the entire return type becomes never. This way, we don’t add any new properties to the return type:
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?{[KinKey]:any}&FormatObj<Rest>:{};
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?{[KinKey]:any}&FormatObj<Rest>:{};
FormatObj<T>工作方式与 相同Record<FormatKeys<T>, any>。我们仍然没有提取任何占位符类型,但是现在我们可以控制整个对象类型,因此我们可以轻松地设置每个占位符的类型。
FormatObj<T> works the same way as Record<FormatKeys<T>, any>. We still didn’t extract any placeholder type, but we made it easy to set the type for each placeholder now that we are in control of the entire object type.
下一步,我们将 中的解析条件更改FormatObj<T>为查找冒号分隔符。如果找到一个:字符,我们将推断 中的后续字符串文字类型Type并将其用作映射键的类型:
As a next step, we change the parsing condition in FormatObj<T> to also look out for colon delimiters. If we find a : character, we infer the subsequent string literal type in Type and use it as the type for the mapped-out key:
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}:${inferType}}${inferRest}`?{[KinKey]:Type}&FormatObj<Rest>:{};
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}:${inferType}}${inferRest}`?{[KinKey]:Type}&FormatObj<Rest>:{};
我们非常接近了;只有一个警告。我们推断出一个字符串文字类型。这意味着,如果我们,例如,解析{age:number},的类型age将是文字字符串"number"。我们需要将此字符串转换为实际类型。我们可以做另一个条件类型或使用映射类型作为查找:
We are very close; there’s just one caveat. We infer a string literal type. This means that if we, for example, parse {age:number}, the type of age would be the literal string "number". We need to convert this string to an actual type. We could do another conditional type or use a map type as a lookup:
typeMapFormatType={string:string;number:number;boolean:boolean;[x:string]:any;};
typeMapFormatType={string:string;number:number;boolean:boolean;[x:string]:any;};
这样,我们可以简单地检查哪种类型与哪个键相关联,并为所有其他字符串提供出色的后备:
That way, we can simply check which type is associated with which key and have a fantastic fallback for all other strings:
typeA=MapFormatType["string"];// stringtypeB=MapFormatType["number"];// numbertypeC=MapFormatType["notavailable"];// any
typeA=MapFormatType["string"];// stringtypeB=MapFormatType["number"];// numbertypeC=MapFormatType["notavailable"];// any
让我们连接MapFormatType到FormatObj<T>:
Let’s wire MapFormatType up to FormatObj<T>:
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}:${inferType}}${inferRest}`?{[KinKey]:MapFormatType[Type]}&FormatObj<Rest>:{};
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}:${inferType}}${inferRest}`?{[KinKey]:MapFormatType[Type]}&FormatObj<Rest>:{};
我们快到了!现在的问题是,我们希望每个占位符也定义一个类型。我们希望将类型设为可选。但我们的解析条件明确要求:分隔符,因此每个未定义类型的占位符也不会产生属性。
We are almost there! The problem now is that we expect every placeholder to also define a type. We want to make types optional. But our parsing condition explicitly asks for : delimiters, so every placeholder that doesn’t define a type doesn’t produce a property, either.
解决方案是在检查占位符之后检查类型:
The solution is to do the check for types after we check for placeholder:
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?Keyextends`${inferKeyPart}:${inferTypePart}`?{[KinKeyPart]:MapFormatType[TypePart]}&FormatObj<Rest>:{[KinKey]:any}&FormatObj<Rest>:{};
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?Keyextends`${inferKeyPart}:${inferTypePart}`?{[KinKeyPart]:MapFormatType[TypePart]}&FormatObj<Rest>:{[KinKey]:any}&FormatObj<Rest>:{};
类型如下:
The type reads as follows:
检查是否有可用的占位符。
Check if there is a placeholder available.
如果有占位符,则检查是否有类型注释。如果有,则将键映射到格式类型;否则,将原始键映射到any。
If a placeholder is available, check if there is a type annotation. If so, map the key to a format type; otherwise, map the original key to any.
在所有其他情况下,返回空对象。
In all other cases, return the empty object.
就是这样。我们可以添加一个故障安全保护。any我们至少可以期望类型实现,而不是允许没有类型定义的占位符使用类型toString()。这确保我们始终获得字符串表示形式:
And that’s it. There is one fail-safe guard that we can add. Instead of allowing any type for placeholders without a type definition, we can at least expect that the type implements toString(). This ensures we always get a string representation:
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?Keyextends`${inferKeyPart}:${inferTypePart}`?{[KinKeyPart]:MapFormatType[TypePart]}&FormatObj<Rest>:{[KinKey]:{toString():string}}&FormatObj<Rest>:{};
typeFormatObj<Textendsstring>=Textends`${string}{${inferKey}}${inferRest}`?Keyextends`${inferKeyPart}:${inferTypePart}`?{[KinKeyPart]:MapFormatType[TypePart]}&FormatObj<Rest>:{[KinKey]:{toString():string}}&FormatObj<Rest>:{};
有了它,让我们应用新类型format并改变实现:
And with that, let’s apply the new type to format and change the implementation:
functionformat<Textendsstring,KextendsFormatObj<T>>(fmtString:T,params:K):string{letret:string=fmtString;for(letkinparams){letval=`${params[k]}`;letsearchPattern=newRegExp(`{${k}:?.*?}`,"g");ret=ret.replaceAll(searchPattern,val);}returnret;}
functionformat<Textendsstring,KextendsFormatObj<T>>(fmtString:T,params:K):string{letret:string=fmtString;for(letkinparams){letval=`${params[k]}`;letsearchPattern=newRegExp(`{${k}:?.*?}`,"g");ret=ret.replaceAll(searchPattern,val);}returnret;}
我们用正则表达式来替换名称,使其具有潜在的类型注释。无需在函数内检查类型。在这种情况下,TypeScript 应该足以提供帮助。
We help ourselves with a regular expression to replace names with potential type annotations. There is no need to check types within the function. TypeScript should be enough to help in this case.
我们看到,条件类型与字符串模板文字类型以及其他工具(如递归和类型查找)相结合,使我们能够用几行代码指定复杂的关系。我们的类型变得更好,我们的代码变得更健壮,开发人员使用这样的 API 是一种乐趣。
What we’ve seen is that conditional types in combination with string template literal types and other tools like recursion and type lookups allow us to specify complex relationships with a couple of lines of code. Our types get better, our code gets more robust, and it’s a joy for developers to use APIs like this.
使用累积技术来实现尾部调用优化。
Use the accumulation technique to enable tail-call optimization.
TypeScript 的字符串模板文字类型与条件类型结合允许您动态创建新的字符串类型,它可以用作属性键或检查程序中是否存在有效字符串。
TypeScript’s string template literal types in combination with conditional types allow you to create new string types on the fly, which can serve as property keys or check your program for valid strings.
它们使用递归工作,这意味着就像一个函数一样,你可以一遍又一遍地调用相同的类型,直到达到一定限度。
They work using recursion, which means that just like a function, you can call the same type over and over again, up to a certain limit.
例如,此类型Trim<T>会删除字符串类型开始和结束处的空格:
For example, this type Trim<T> removes whitespaces at the start and end of your string type:
typeTrim<Textendsstring>=Textends`${inferX}`?Trim<X>:Textends`${inferX}`?Trim<X>:T;
typeTrim<Textendsstring>=Textends`${inferX}`?Trim<X>:Textends`${inferX}`?Trim<X>:T;
它检查开头是否有空格,推断其余部分,然后再次进行相同的检查。一旦开头的所有空格都消失了,就会对结尾的空格进行相同的检查。一旦开头和结尾的所有空格都消失了,它就完成了,并跳转到最后一个分支——返回剩余的字符串:
It checks if there’s a whitespace at the beginning, infers the rest, and does the same check over again. Once all whitespaces at the beginning are gone, the same checks happen for whitespaces at the end. Once all whitespaces at the beginning and end are gone, it is finished and hops into the last branch—returning the remaining string:
typeTrimmed=Trim<" key ">;// "key"
typeTrimmed=Trim<" key ">;// "key"
反复调用该类型就是递归,这样编写效果相当不错。TypeScript 可以从类型中看到递归调用是独立的,并且可以将它们评估为尾部调用优化,这意味着它可以在同一调用堆栈框架内评估递归的下一步。
Calling the type over and over is recursion, and writing it like that works reasonably well. TypeScript can see from the type that the recursive calls stand on their own, and it can evaluate them as tail-call optimized, which means it can evaluate the next step of the recursion within the same call stack frame.
如果你想了解更多关于 JavaScript 中的调用堆栈, Thomas Hunter 的书《使用 Node.js 的分布式系统》(O'Reilly)给出了很好的介绍。
If you want to know more about the call stack in JavaScript, Thomas Hunter’s book Distributed Systems with Node.js (O’Reilly) gives a great introduction.
我们希望使用 TypeScript 的功能以递归方式调用条件类型,通过删除空格和无效字符从任何字符串中创建有效的字符串标识符 。
We want to use TypeScript’s feature to recursively call conditional types to create a valid string identifier out of any string, by removing whitespace and invalid characters.
首先,我们编写一个类似的辅助类型,Trim<T>删除它发现的任何空格:
First, we write a helper type similar to Trim<T> that gets rid of any whitespace it finds:
typeRemoveWhiteSpace<Textendsstring>=Textends`${inferA}${inferB}`?RemoveWhiteSpace<`${Uncapitalize<A>}${Capitalize<B>}`>:T;
typeRemoveWhiteSpace<Textendsstring>=Textends`${inferA}${inferB}`?RemoveWhiteSpace<`${Uncapitalize<A>}${Capitalize<B>}`>:T;
它检查是否存在空格,推断空格前面和空格后面的字符串(可以是空字符串),然后使用新形成的字符串类型再次调用同一类型。它还会将第一个推断的首字母改为小写,将第二个推断的首字母改为大写,以创建类似驼峰式命名的字符串标识符。
It checks if there is a whitespace, infers the strings in front of the whitespace and after the whitespace (which can be empty strings), and calls the same type again with a newly formed string type. It also uncapitalizes the first inference and capitalizes the second inference to create a camel-case-like string identifier.
它会一直这样做,直到所有空格都消失:
It does so until all whitespaces are gone:
typeIdentifier=RemoveWhiteSpace<"Hello World!">;// "helloWorld!"
typeIdentifier=RemoveWhiteSpace<"Hello World!">;// "helloWorld!"
接下来,我们要检查剩余的字符是否有效。我们再次使用递归来获取有效字符的字符串,将它们拆分为只有一个字符的单个字符串类型,并创建大写和非大写版本:
Next, we want to check if the remaining characters are valid. We again use recursion to take a string of valid characters, split them into single string types with only one character, and create a capitalized and uncapitalized version:
typeStringSplit<Textendsstring>=Textends`${inferChar}${inferRest}`?Capitalize<Char>|Uncapitalize<Char>|StringSplit<Rest>:never;typeChars=StringSplit<"abcdefghijklmnopqrstuvwxyz">;// "a" | "A" | "b" | "B" | "c" | "C" | "d" | "D" | "e" | "E" |// "f" | "F" | "g" | "G" | "h" | "H" | "i" | "I" | "j" | "J" |// "k" | "K" | "l" | "L" | "m" | "M" | "n" | "N" | "o" | "O" |// "p" | "P" | "q" | "Q" | "r" | "R" | "s" | "S" | "t" | "T" |// "u" | "U" | "v" | "V" | "w" | "W" | "x" | "X" | "y" | "Y" |// "z" | "Z"
typeStringSplit<Textendsstring>=Textends`${inferChar}${inferRest}`?Capitalize<Char>|Uncapitalize<Char>|StringSplit<Rest>:never;typeChars=StringSplit<"abcdefghijklmnopqrstuvwxyz">;// "a" | "A" | "b" | "B" | "c" | "C" | "d" | "D" | "e" | "E" |// "f" | "F" | "g" | "G" | "h" | "H" | "i" | "I" | "j" | "J" |// "k" | "K" | "l" | "L" | "m" | "M" | "n" | "N" | "o" | "O" |// "p" | "P" | "q" | "Q" | "r" | "R" | "s" | "S" | "t" | "T" |// "u" | "U" | "v" | "V" | "w" | "W" | "x" | "X" | "y" | "Y" |// "z" | "Z"
我们删除找到的第一个字符,将其大写,将其小写,然后对其余字符执行相同操作,直到没有剩余字符串。请注意,此递归无法进行尾部调用优化,因为我们将递归调用与每个递归步骤的结果放在联合类型中。当我们达到 50 个字符时,我们将达到递归限制(TypeScript 编译器的硬性限制)。使用基本字符,我们就可以了!
We shave off the first character we find, capitalize it, uncapitalize it, and do the same with the rest until no more strings are left. Note that this recursion can’t be tail-call optimized, as we put the recursive call in a union type with the results from each recursion step. Here we would reach a recursion limit when we hit 50 characters (a hard limit from the TypeScript compiler). With basic characters, we are fine!
但是,当我们进行下一步,即创建时,我们遇到了第一个限制Identifier。在这里,我们检查有效字符。首先,我们调用RemoveWhiteSpace<T>类型,这使我们能够摆脱空格和驼峰式其余部分。然后我们根据有效字符检查结果。
But we hit the first limits when we are doing the next step, the creation of the Identifier. Here we check for valid characters. First, we call the RemoveWhiteSpace<T> type, which allows us to get rid of whitespaces and camel-cases the rest. Then we check the result against valid characters.
就像在 中一样StringSplit<T>,我们删除第一个字符,但在推理中再做一次类型检查。我们看看刚刚删除的字符是否是有效字符之一。然后我们得到其余部分。我们再次组合相同的字符串,但对剩余的字符串进行递归检查。如果第一个字符无效,则我们CreateIdentifier<T>使用其余部分进行调用:
Just like in StringSplit<T>, we shave off the first character but do another type-check within inference. We see if the character we just shaved off is one of the valid characters. Then we get the rest. We combine the same string again but do a recursive check with the remaining string. If the first character isn’t valid, then we call CreateIdentifier<T> with the rest:
typeCreateIdentifier<Textendsstring>=RemoveWhiteSpace<T>extends`${inferAextendsChars}${inferRest}`?`${A}${CreateIdentifier<Rest>}`// ^ Type instantiation is excessively deep and possibly infinite.(2589)_.:RemoveWhiteSpace<T>extends`${inferA}${inferRest}`?CreateIdentifier<Rest>:T;
typeCreateIdentifier<Textendsstring>=RemoveWhiteSpace<T>extends`${inferAextendsChars}${inferRest}`?`${A}${CreateIdentifier<Rest>}`// ^ Type instantiation is excessively deep and possibly infinite.(2589)_.:RemoveWhiteSpace<T>extends`${inferA}${inferRest}`?CreateIdentifier<Rest>:T;
在这里我们遇到了第一个递归限制。TypeScript 会发出错误警告,警告我们此类型实例化可能无限且过深。似乎如果我们在字符串模板文字类型中使用递归调用,这可能会导致调用堆栈错误并崩溃。所以 TypeScript 崩溃了。它无法在此处进行尾部调用优化。
And here we hit the first recursion limit. TypeScript warns us—with an error—that this type instantiation is possibly infinite and excessively deep. It seems that if we use the recursive call within a string template literal type, this might result in call stack errors and blow up. So TypeScript breaks. It can’t do tail-call optimization here.
CreateIdentifier<T>即使 TypeScript 在您编写类型时出错,也可能仍会产生正确的结果。这些错误很难发现,因为它们可能会在您不期望的时候出现。请确保在发生错误时不要让 TypeScript 产生任何结果。
CreateIdentifier<T> might still produce correct results, even though TypeScript errors when you write your type. Those are hard-to-spot bugs because they might hit you when you don’t expect them. Be sure to not let TypeScript produce any results when errors happen.
有一种方法可以解决这个问题。要激活尾部调用优化,递归调用需要独立存在。我们可以通过使用所谓的累加器技术来实现这一点。在这里,我们传递了第二个类型参数,称为Acc,它是一种类型string,并用空字符串实例化。我们将其用作累加器,在其中存储中间结果,并将其一遍又一遍地传递给下一个调用:
There’s one way to work around it. To activate tail-call optimization, the recursive call needs to stand alone. We can achieve this by using the so-called accumulator technique. Here, we pass a second type parameter called Acc, which is of a type string and is instantiated with the empty string. We use this as an accumulator where we store the intermediate result, passing it over and over again to the next call:
typeCreateIdentifier<Textendsstring,Accextendsstring="">=RemoveWhiteSpace<T>extends`${inferAextendsChars}${inferRest}`?CreateIdentifier<Rest,`${Acc}${A}`>:RemoveWhiteSpace<T>extends`${inferA}${inferRest}`?CreateIdentifier<Rest,Acc>:Acc;
typeCreateIdentifier<Textendsstring,Accextendsstring="">=RemoveWhiteSpace<T>extends`${inferAextendsChars}${inferRest}`?CreateIdentifier<Rest,`${Acc}${A}`>:RemoveWhiteSpace<T>extends`${inferA}${inferRest}`?CreateIdentifier<Rest,Acc>:Acc;
这样,递归调用又可以独立运行了,结果就是第二个参数。当我们完成递归调用(递归中断分支)时,我们返回累加器,因为它就是我们最终的结果:
This way, the recursive call is standing on its own again, and the result is the second parameter. When we are done with recursive calls, the recursion-breaking branch, we return the accumulator, as it is our finished result:
typeIdentifier=CreateIdentifier<"Hello Wor!ld!">;// "helloWorld"
typeIdentifier=CreateIdentifier<"Hello Wor!ld!">;// "helloWorld"
可能有更巧妙的方法从任何字符串生成标识符,但请注意,同样的事情可能会在您使用递归的任何复杂条件类型中对您造成深远影响。累加器技术是缓解此类问题的好方法。
There might be more clever ways to produce identifiers from any string, but note that the same thing can hit you deep down in any elaborate conditional type where you use recursion. The accumulator technique is a good way to mitigate problems like this.
使用字符串模板文字作为判别联合的判别式。
Use string template literals as discriminants for a discriminated union.
从后端获取数据的方式始终遵循相同的结构。您发出请求,然后等待它被满足并返回一些数据(成功)或被拒绝并返回错误。例如,要登录用户,所有可能的状态可能如下所示:
The way you fetch data from a backend always follows the same structure. You do a request, and it’s pending to be either fulfilled and return some data—success—or rejected and return with an error. For example, to log in a user, all possible states can look like this:
typeUserRequest=|{state:"USER_PENDING";}|{state:"USER_ERROR";message:string;}|{state:"USER_SUCCESS";data:User;};
typeUserRequest=|{state:"USER_PENDING";}|{state:"USER_ERROR";message:string;}|{state:"USER_SUCCESS";data:User;};
当我们获取用户的订单时,我们可以使用相同的状态。唯一的区别在于成功负载和每个状态的名称,这些名称是根据请求类型定制的:
When we fetch a user’s order, we have the same states available. The only difference is in the success payload and in the names of each state, which are tailored to the type of request:
typeOrderRequest=|{state:"ORDER_PENDING";}|{state:"ORDER_ERROR";message:string;}|{state:"ORDER_SUCCESS";data:Order;};
typeOrderRequest=|{state:"ORDER_PENDING";}|{state:"ORDER_ERROR";message:string;}|{state:"ORDER_SUCCESS";data:Order;};
当我们处理全局状态处理机制(例如Redux)时,我们希望使用这样的标识符进行区分。我们仍然希望将其缩小到相应的状态类型!
When we deal with a global state handling mechanism, such as Redux, we want to differentiate by using identifiers like this. We still want to narrow it to the respective state types!
TypeScript 允许你创建判别联合类型,其中判别式是字符串模板文字类型。因此,我们可以使用相同的模式总结所有可能的后端请求:
TypeScript allows you to create discriminated union types where the discriminant is a string template literal type. So we can sum up all possible backend requests using the same pattern:
typePending={state:`${Uppercase<string>}_PENDING`;};typeErr={state:`${Uppercase<string>}_ERROR`;message:string;};typeSuccess={state:`${Uppercase<string>}_SUCCESS`;data:any;};typeBackendRequest=Pending|Err|Success;
typePending={state:`${Uppercase<string>}_PENDING`;};typeErr={state:`${Uppercase<string>}_ERROR`;message:string;};typeSuccess={state:`${Uppercase<string>}_SUCCESS`;data:any;};typeBackendRequest=Pending|Err|Success;
这已经给了我们一个优势。我们知道每个联合类型成员的状态属性需要以大写字符串开头,后跟下划线和 相应的状态字符串。我们可以像往常一样将其缩小到子类型 :
This already gives us an edge. We know that the state property of each union type member needs to start with an uppercase string, followed by an underscore and the respective state as a string. And we can narrow it to the subtypes just as we are used to:
functionexecute(req:BackendRequest){switch(req.state){case"USER_PENDING":// req: Pendingconsole.log("Login pending...");break;case"USER_ERROR":// req: ErrthrownewError(`Login failed:${req.message}`);case"USER_SUCCESS":// req: Successlogin(req.data);break;case"ORDER_PENDING":// req: Pendingconsole.log("Fetching orders pending");break;case"ORDER_ERROR":// req: ErrthrownewError(`Fetching orders failed:${req.message}`);case"ORDER_SUCCESS":// req: SuccessdisplayOrder(req.data);break;}}
functionexecute(req:BackendRequest){switch(req.state){case"USER_PENDING":// req: Pendingconsole.log("Login pending...");break;case"USER_ERROR":// req: ErrthrownewError(`Login failed:${req.message}`);case"USER_SUCCESS":// req: Successlogin(req.data);break;case"ORDER_PENDING":// req: Pendingconsole.log("Fetching orders pending");break;case"ORDER_ERROR":// req: ErrthrownewError(`Fetching orders failed:${req.message}`);case"ORDER_SUCCESS":// req: SuccessdisplayOrder(req.data);break;}}
将整个字符串集作为判别式的第一部分可能有点太多了。我们可以对各种已知请求进行子集化,并使用字符串操作类型来获取正确的子类型:
Having the entire set of strings as the first part of the discriminant might be a bit too much. We can subset to a variety of known requests and use string manipulation types to get the right subtypes:
typeRequestConstants="user"|"order";typePending={state:`${Uppercase<RequestConstants>}_PENDING`;};typeErr={state:`${Uppercase<RequestConstants>}_ERROR`;message:string;};typeSuccess={state:`${Uppercase<RequestConstants>}_SUCCESS`;data:any;};
typeRequestConstants="user"|"order";typePending={state:`${Uppercase<RequestConstants>}_PENDING`;};typeErr={state:`${Uppercase<RequestConstants>}_ERROR`;message:string;};typeSuccess={state:`${Uppercase<RequestConstants>}_SUCCESS`;data:any;};
这就是如何消除拼写错误的方法!甚至更好,假设我们将所有数据存储在类型为 的全局状态对象中。我们可以从这里Data派生出所有可能的类型。通过使用,我们得到组成状态的字符串键:BackendRequestkeyof DataBackendRequest
That’s how to get rid of typos! Even better, let’s say we store all data in a global state object of type Data. We can derive all possible BackendRequest types from here. By using keyof Data, we get the string keys that make up the BackendRequest state:
typeData={user:User|null;order:Order|null;};typeRequestConstants=keyofData;typePending={state:`${Uppercase<RequestConstants>}_PENDING`;};typeErr={state:`${Uppercase<RequestConstants>}_ERROR`;message:string;};
typeData={user:User|null;order:Order|null;};typeRequestConstants=keyofData;typePending={state:`${Uppercase<RequestConstants>}_PENDING`;};typeErr={state:`${Uppercase<RequestConstants>}_ERROR`;message:string;};
Pending对于和来说,这已经运行良好了Err,但在这种Success情况下,我们想要与"user"或相关的实际数据类型"order"。
This already works well for Pending and Err, but in the Success case we want to have the actual data type associated with "user" or "order".
第一个选择是使用索引访问data从以下位置获取属性的正确类型Data:
A first option would be to use index access to get the correct types for the data property from Data:
typeSuccess={state:`${Uppercase<RequestConstants>}_SUCCESS`;data:NonNullable<Data[RequestConstants]>;};
typeSuccess={state:`${Uppercase<RequestConstants>}_SUCCESS`;data:NonNullable<Data[RequestConstants]>;};
NonNullable<T>删除联合类型中的null和。启用编译器标志后,和
都会从所有类型中排除。这意味着,如果您有空值状态,则需要手动添加它们;如果要确保不存在空值状态,则需要手动排除它们。undefinedstrictNullChecksnullundefined
NonNullable<T> gets rid of null and undefined in a union type. With the compiler flag strictNullChecks on, both null and
undefined are excluded from all types. This means you need to manually add them if you have nullish states and manually exclude them when you want to make sure that they don’t.
但这意味着 可以data是两者User或Order所有后端请求,如果我们添加新的请求,则更多。为了避免破坏标识符与其关联数据类型之间的连接,我们映射所有RequestConstants,创建状态对象,然后再次使用 的索引访问RequestConstants来生成联合类型:
But this would mean that data can be both User or Order for all backend requests, and more if we add new ones. To avoid breaking the connection between the identifier and its associated data type, we map through all RequestConstants, create state objects, and then use index access of RequestConstants again to produce a union type:
typeSuccess={[KinRequestConstants]:{state:`${Uppercase<K>}_SUCCESS`;data:NonNullable<Data[K]>;};}[RequestConstants];
typeSuccess={[KinRequestConstants]:{state:`${Uppercase<K>}_SUCCESS`;data:NonNullable<Data[K]>;};}[RequestConstants];
Success is now equal to the manually created union type:
typeSuccess={state:"USER_SUCCESS";data:User;}|{state:"ORDER_SUCCESS";data:Order;};
typeSuccess={state:"USER_SUCCESS";data:User;}|{state:"ORDER_SUCCESS";data:Order;};
元组类型是具有固定长度的数组,其中每个元素的类型都有定义。元组在 React 等库中被广泛使用,因为它易于解构和命名元素,但在 React 之外,它们也被认为是对象的良好替代品。
Tuple types are arrays with a fixed length and where every type of each element is defined. Tuples are heavily used in libraries like React as it’s easy to destructure and name elements, but outside of React they also have gained recognition as a nice alternative to objects.
可变元组类型是一种具有相同属性的元组类型——定义的长度和每个元素的类型都是已知的——但确切的形状尚未定义。它们基本上告诉类型系统将会有一些元素,但我们还不知道它们会是哪些元素。它们是通用的,旨在用真实类型替换。
A variadic tuple type is a tuple type that has the same properties—defined length and the type of each element is known—but where the exact shape is yet to be defined. They basically tell the type system that there will be some elements, but we don’t know yet which ones they will be. They are generic and meant to be substituted with real types.
当我们了解到元组类型也可用于描述函数签名时,这个听起来相当无聊的功能就变得令人兴奋得多,因为元组可以作为参数分散到函数调用中。这意味着我们可以使用可变元组类型从函数和函数调用以及接受函数作为参数的函数中获取最多的信息。
What sounds like a fairly boring feature is much more exciting when we understand that tuple types can also be used to describe function signatures, as tuples can be spread out to function calls as arguments. This means we can use variadic tuple types to get the most information out of functions and function calls, and functions that accept functions as parameters.
本章提供了很多关于如何使用可变元组类型的用例,描述了几种使用函数作为参数并需要从中获取最多信息的场景。如果没有可变元组类型,这些场景将很难实现,甚至根本不可能实现。读完之后,你会发现可变元组类型是函数式编程模式的一个关键特性。
This chapter provides a lot of use cases on how we can use variadic tuple types to describe several scenarios where we use functions as parameters and need to get the most information from them. Without variadic tuple types, these scenarios would be hard to develop or outright impossible. After reading through, you will see variadic tuple types as a key feature for functional programming patterns.
使用可变元组类型。
Use variadic tuple types.
concat是一个可爱的辅助函数,它接受两个数组并将它们组合起来。它使用数组扩展,简短、美观且可读性强:
concat is a lovely helper function that takes two arrays and combines them. It uses array spreading and is short, nice, and readable:
functionconcat(arr1,arr2){return[...arr1,...arr2];}
functionconcat(arr1,arr2){return[...arr1,...arr2];}
为该函数创建类型可能很困难,特别是当您对类型有某些期望时。传入两个数组很容易,但返回类型应该是什么样的?您是否对返回的单个数组类型感到满意,或者您是否想知道此数组中每个元素的类型?
Creating types for this function can be hard, especially if you have certain expectations from your types. Passing in two arrays is easy, but what should the return type look like? Are you happy with a single array type in return, or do you want to know the types of each element in this array?
我们选择后者:我们想要元组,所以我们知道传递给此函数的每个元素的类型。为了正确地键入这样的函数,以便它考虑到所有可能的边缘情况,我们最终会陷入重载的海洋:
Let’s go for the latter: we want tuples so we know the type of each element we pass to this function. To correctly type a function like this so that it takes all possible edge cases into account, we would end up in a sea of overloads:
// 7 overloads for an empty second arrayfunctionconcat(arr1:[],arr2:[]):[];functionconcat<A>(arr1:[A],arr2:[]):[A];functionconcat<A,B>(arr1:[A,B],arr2:[]):[A,B];functionconcat<A,B,C>(arr1:[A,B,C],arr2:[]):[A,B,C];functionconcat<A,B,C,D>(arr1:[A,B,C,D],arr2:[]):[A,B,C,D];functionconcat<A,B,C,D,E>(arr1:[A,B,C,D,E],arr2:[]):[A,B,C,D,E];functionconcat<A,B,C,D,E,F>(arr1:[A,B,C,D,E,F],arr2:[]):[A,B,C,D,E,F];// 7 more for arr2 having one elementfunctionconcat<A2>(arr1:[],arr2:[A2]):[A2];functionconcat<A1,A2>(arr1:[A1],arr2:[A2]):[A1,A2];functionconcat<A1,B1,A2>(arr1:[A1,B1],arr2:[A2]):[A1,B1,A2];functionconcat<A1,B1,C1,A2>(arr1:[A1,B1,C1],arr2:[A2]):[A1,B1,C1,A2];functionconcat<A1,B1,C1,D1,A2>(arr1:[A1,B1,C1,D1],arr2:[A2]):[A1,B1,C1,D1,A2];functionconcat<A1,B1,C1,D1,E1,A2>(arr1:[A1,B1,C1,D1,E1],arr2:[A2]):[A1,B1,C1,D1,E1,A2];functionconcat<A1,B1,C1,D1,E1,F1,A2>(arr1:[A1,B1,C1,D1,E1,F1],arr2:[A2]):[A1,B1,C1,D1,E1,F1,A2];// and so on, and so forth
// 7 overloads for an empty second arrayfunctionconcat(arr1:[],arr2:[]):[];functionconcat<A>(arr1:[A],arr2:[]):[A];functionconcat<A,B>(arr1:[A,B],arr2:[]):[A,B];functionconcat<A,B,C>(arr1:[A,B,C],arr2:[]):[A,B,C];functionconcat<A,B,C,D>(arr1:[A,B,C,D],arr2:[]):[A,B,C,D];functionconcat<A,B,C,D,E>(arr1:[A,B,C,D,E],arr2:[]):[A,B,C,D,E];functionconcat<A,B,C,D,E,F>(arr1:[A,B,C,D,E,F],arr2:[]):[A,B,C,D,E,F];// 7 more for arr2 having one elementfunctionconcat<A2>(arr1:[],arr2:[A2]):[A2];functionconcat<A1,A2>(arr1:[A1],arr2:[A2]):[A1,A2];functionconcat<A1,B1,A2>(arr1:[A1,B1],arr2:[A2]):[A1,B1,A2];functionconcat<A1,B1,C1,A2>(arr1:[A1,B1,C1],arr2:[A2]):[A1,B1,C1,A2];functionconcat<A1,B1,C1,D1,A2>(arr1:[A1,B1,C1,D1],arr2:[A2]):[A1,B1,C1,D1,A2];functionconcat<A1,B1,C1,D1,E1,A2>(arr1:[A1,B1,C1,D1,E1],arr2:[A2]):[A1,B1,C1,D1,E1,A2];functionconcat<A1,B1,C1,D1,E1,F1,A2>(arr1:[A1,B1,C1,D1,E1,F1],arr2:[A2]):[A1,B1,C1,D1,E1,F1,A2];// and so on, and so forth
而且这只考虑了最多有六个元素的数组。像这样用重载来输入函数的组合实在是太累了。不过还有一种更简单的方法:可变元组类型。
And this only takes into account arrays that have up to six elements. The combinations for typing a function like this with overloads is exhausting. But there is an easier way: variadic tuple types.
A tuple type in TypeScript is an array with the following features:
数组的长度已定义。
The length of the array is defined.
每个元素的类型都是已知的(并且不必相同)。
The type of each element is known (and does not have to be the same).
例如,这是一个元组类型:
For example, this is a tuple type:
typePersonProps=[string,number];const[name,age]:PersonProps=['Stefan',37];
typePersonProps=[string,number];const[name,age]:PersonProps=['Stefan',37];
可变元组类型是一种具有相同属性的元组类型(定义的长度和每个元素的类型已知),但确切的形状尚未定义。由于我们尚不知道类型和长度,因此我们只能在泛型中使用可变元组类型:
A variadic tuple type is a tuple type that has the same properties—defined length and the type of each element is known—but where the exact shape is yet to be defined. Since we don’t know the type and length yet, we can only use variadic tuple types in generics:
typeFoo<Textendsunknown[]>=[string,...T,number];typeT1=Foo<[boolean]>;// [string, boolean, number]typeT2=Foo<[number,number]>;// [string, number, number, number]typeT3=Foo<[]>;// [string, number]
typeFoo<Textendsunknown[]>=[string,...T,number];typeT1=Foo<[boolean]>;// [string, boolean, number]typeT2=Foo<[number,number]>;// [string, number, number, number]typeT3=Foo<[]>;// [string, number]
这与函数中的其余元素类似,但最大的区别在于可变元组类型可以在元组中的任何地方出现,并且多次出现:
This is similar to rest elements in functions, but the big difference is that variadic tuple types can happen anywhere in the tuple, and multiple times:
typeBar<Textendsunknown[],Uextendsunknown[]>=[...T,string,...U];typeT4=Bar<[boolean],[number]>;// [boolean, string, number]typeT5=Bar<[number,number],[boolean]>;// [number, number, string, boolean]typeT6=Bar<[],[]>;// [string]
typeBar<Textendsunknown[],Uextendsunknown[]>=[...T,string,...U];typeT4=Bar<[boolean],[number]>;// [boolean, string, number]typeT5=Bar<[number,number],[boolean]>;// [number, number, string, boolean]typeT6=Bar<[],[]>;// [string]
当我们将其应用于concat函数时,我们必须引入两个泛型参数,每个数组一个。两者都需要限制为数组。然后,我们可以创建一个返回类型,将两种数组类型组合在新创建的元组类型中:
When we apply this to the concat function, we have to introduce two generic parameters, one for each array. Both need to be constrained to arrays. Then, we can create a return type that combines both array types in a newly created tuple type:
functionconcat<Textendsunknown[],Uextendsunknown[]>(arr1:T,arr2:U):[...T,...U]{return[...arr1,...arr2];}// const test: (string | number)[]consttest=concat([1,2,3],[6,7,"a"]);
functionconcat<Textendsunknown[],Uextendsunknown[]>(arr1:T,arr2:U):[...T,...U]{return[...arr1,...arr2];}// const test: (string | number)[]consttest=concat([1,2,3],[6,7,"a"]);
语法非常漂亮;它与 JavaScript 中的实际连接非常相似。结果也非常好:我们得到了一个(string | number)[],这已经是我们可以处理的东西了。
The syntax is beautiful; it’s very similar to the actual concatenation in JavaScript. The result is also really good: we get a (string | number)[], which is already something we can work with.
但是我们使用的是元组类型。如果我们想确切地知道要连接哪些元素,我们必须将数组类型转换为元组类型,方法是将通用数组类型展开为元组类型:
But we work with tuple types. If we want to know exactly which elements we are concatenating, we have to transform the array types into tuple types, by spreading out the generic array type into a tuple type:
functionconcat<Textendsunknown[],Uextendsunknown[]>(arr1:[...T],arr2:[...U]):[...T,...U]{return[...arr1,...arr2];}
functionconcat<Textendsunknown[],Uextendsunknown[]>(arr1:[...T],arr2:[...U]):[...T,...U]{return[...arr1,...arr2];}
这样,我们还得到了一个元组类型:
And with that, we also get a tuple type in return:
// const test: [number, number, number, number, number, string]consttest=concat([1,2,3],[6,7,"a"]);
// const test: [number, number, number, number, number, string]consttest=concat([1,2,3],[6,7,"a"]);
好消息是我们没有失去任何东西。如果我们传递数组,而我们事先不知道每个元素,我们仍然会得到数组类型:
The good news is that we don’t lose anything. If we pass arrays where we don’t know each element up front, we still get array types in return:
declareconsta:string[]declareconstb:number[]// const test: (string | number)[]consttest=concat(a,b);
declareconsta:string[]declareconstb:number[]// const test: (string | number)[]consttest=concat(a,b);
能够用单一类型描述这种行为肯定比在函数重载中编写所有可能的组合更加灵活和可读。
Being able to describe this behavior in a single type is definitely much more flexible and readable than writing every possible combination in a function overload.
函数参数是元组类型。使用可变元组类型使它们具有通用性。
Function arguments are tuple types. Make them generic using variadic tuple types.
在 JavaScript 中引入 Promises 之前,使用回调进行异步编程非常常见。函数通常会采用一系列参数,然后是返回结果后执行的回调函数,例如加载文件或执行非常简单的 HTTP 请求的函数:
Before Promises were a thing in JavaScript it was very common to do asynchronous programming using callbacks. Functions would usually take a list of arguments, followed by a callback function that would be executed once the results were there, such as functions to load a file or do a very simplified HTTP request:
functionloadFile(filename:string,encoding:string,callback:(result:File)=>void){// TODO}loadFile("./data.json","utf-8",(result)=>{// do something with the file});functionrequest(url:URL,callback:(result:JSON)=>void){// TODO}request("https://typescript-cookbook.com",(result)=>{// TODO});
functionloadFile(filename:string,encoding:string,callback:(result:File)=>void){// TODO}loadFile("./data.json","utf-8",(result)=>{// do something with the file});functionrequest(url:URL,callback:(result:JSON)=>void){// TODO}request("https://typescript-cookbook.com",(result)=>{// TODO});
两者都遵循相同的模式:首先是参数,最后是带有结果的回调。这种方法有效,但如果您有大量异步调用,导致回调中又出现回调,这种方法就很笨拙了,也称为“末日金字塔”:
Both follow the same pattern: arguments first, a callback with the result last. This works but can be clumsy if you have lots of asynchronous calls that result in callbacks within callbacks, also known as the “the pyramid of doom”:
loadFile("./data.txt","utf-8",(file)=>{// pseudo APIfile.readText((url)=>{request(url,(data)=>{// do something with data})})})
loadFile("./data.txt","utf-8",(file)=>{// pseudo APIfile.readText((url)=>{request(url,(data)=>{// do something with data})})})
Promises 可以解决这个问题。它们不仅找到了一种链接异步调用而不是嵌套的方法,而且还是async/的网关await,允许我们以同步形式编写异步代码:
Promises take care of that. Not only do they find a way to chain asynchronous calls instead of nesting them, they also are the gateway for async/await, allowing us to write asynchronous code in a synchronous form:
loadFilePromise("./data.txt","utf-8").then((file)=>file.text()).then((url)=>request(url)).then((data)=>{// do something with data});// with async/awaitconstfile=awaitloadFilePromise("./data.txt"."utf-8");consturl=awaitfile.text();constdata=awaitrequest(url);// do something with data.
loadFilePromise("./data.txt","utf-8").then((file)=>file.text()).then((url)=>request(url)).then((data)=>{// do something with data});// with async/awaitconstfile=awaitloadFilePromise("./data.txt"."utf-8");consturl=awaitfile.text();constdata=awaitrequest(url);// do something with data.
太棒了!值得庆幸的是,可以将每个遵循回调模式的函数转换为Promise。我们希望创建一个promisify函数来自动为我们执行此操作:
Much nicer! Thankfully, it is possible to convert every function that adheres to the callback pattern to a Promise. We want to create a promisify function to do that for us automatically:
functionpromisify(fn:unknown):Promise<unknown>{// To be implemented}constloadFilePromise=promisify(loadFile);constrequestPromise=promisify(request);
functionpromisify(fn:unknown):Promise<unknown>{// To be implemented}constloadFilePromise=promisify(loadFile);constrequestPromise=promisify(request);
但是我们如何输入这些内容呢?可变元组类型可以帮到你!
But how do we type this? Variadic tuple types to the rescue!
每个函数头都可以描述为一个元组类型。例如:
Every function head can be described as a tuple type. For example:
declarefunctionhello(name:string,msg:string):void;
declarefunctionhello(name:string,msg:string):void;
与以下相同:
is the same as:
declarefunctionhello(...args:[string,string]):void;
declarefunctionhello(...args:[string,string]):void;
我们可以非常灵活地定义它:
And we can be very flexible in defining it:
declarefunctionh(a:string,b:string,c:string):void;// equal todeclarefunctionh(a:string,b:string,...r:[string]):void;// equal todeclarefunctionh(a:string,...r:[string,string]):void;// equal todeclarefunctionh(...r:[string,string,string]):void;
declarefunctionh(a:string,b:string,c:string):void;// equal todeclarefunctionh(a:string,b:string,...r:[string]):void;// equal todeclarefunctionh(a:string,...r:[string,string]):void;// equal todeclarefunctionh(...r:[string,string,string]):void;
这也被称为休息元素,是 JavaScript 中一种允许您定义具有几乎无限参数列表的函数的功能,其中最后一个元素(即休息元素)会吸收所有多余的参数。
This is also known as a rest element, something we have in JavaScript that allows you to define functions with an almost limitless argument list, where the last element, the rest element, sucks all excess arguments in.
例如,这个通用元组函数接受任意类型的参数列表并 根据它创建一个元组:
For example, this generic tuple function takes an argument list of any type and creates a tuple out of it:
functiontuple<Textendsany[]>(...args:T):T{returnargs;}constnumbers:number[]=getArrayOfNumbers();constt1=tuple("foo",1,true);// [string, number, boolean]constt2=tuple("bar",...numbers);// [string, ...number[]]
functiontuple<Textendsany[]>(...args:T):T{returnargs;}constnumbers:number[]=getArrayOfNumbers();constt1=tuple("foo",1,true);// [string, number, boolean]constt2=tuple("bar",...numbers);// [string, ...number[]]
问题是,剩余元素始终必须放在最后。在 JavaScript 中,不可能在中间某处定义几乎无穷无尽的参数列表。但是,使用可变元组类型,我们可以在 TypeScript 中做到这一点!
The thing is, rest elements always have to be last. In JavaScript, it’s not possible to define an almost endless argument list somewhere in between. With variadic tuple types, however, we can do this in TypeScript!
我们再看一下loadFile和request函数。如果我们将两个函数的参数描述为元组,它们将如下所示:
Let’s look again at the loadFile and request functions again. If we described the parameters of both functions as tuples, they would look like this:
functionloadFile(...args:[string,string,(result:File)=>void]){// TODO}functionrequest2(...args:[URL,(result:JSON)=>void]){// TODO}
functionloadFile(...args:[string,string,(result:File)=>void]){// TODO}functionrequest2(...args:[URL,(result:JSON)=>void]){// TODO}
让我们来找找相似之处。两者都以结果类型不同的回调结束。我们可以通过将变体替换为泛型来对齐两个回调的类型。稍后,在使用中,我们用泛型替换实际类型。因此JSON和File成为泛型类型参数Res。
Let’s look for similarities. Both end with a callback with a varying result type. We can align the types for both callbacks by substituting the variations with a generic one. Later, in usage, we substitute generics for actual types. So JSON and File become the generic type parameter Res.
现在来看看之前的 参数Res。它们可以说是完全不同的,但它们也有一些共同点:它们都是元组中的元素。这就需要一个可变元组。我们知道它们会有具体的长度和具体的类型,但现在我们只是为它们取一个占位符。我们把它们称为Args。
Now for the parameters before Res. They are arguably totally different, but even they have something in common: they are elements within a tuple. This calls for a variadic tuple. We know they will have a concrete length and concrete types, but right now we just take a placeholder for them. Let’s call them Args.
因此,描述两个函数签名的函数类型可能如下所示:
So a function type describing both function signatures could look like this:
typeFn<Argsextendsunknown[],Res>=(...args:[...Args,(result:Res)=>void])=>void;
typeFn<Argsextendsunknown[],Res>=(...args:[...Args,(result:Res)=>void])=>void;
尝试一下你的新类型:
Take your new type for a spin:
typeLoadFileFn=Fn<[string,string],File>;typeRequestFn=Fn<[URL],JSON>;
typeLoadFileFn=Fn<[string,string],File>;typeRequestFn=Fn<[URL],JSON>;
这正是我们promisify函数所需要的。我们能够提取所有相关参数(回调之前的参数和结果类型),并将它们按新顺序排列。
This is exactly what we need for the promisify function. We are able to extract all relevant parameters—the ones before the callback and the result type—and bring them into a new order.
让我们首先将新创建的函数类型直接内联到函数
签名中promisify:
Let’s start by inlining the newly created function type directly into the function
signature of promisify:
functionpromisify<Argsextendsunknown[],Res>(fn:(...args:[...Args,(result:Res)=>void])=>void):(...args:Args)=>Promise<Res>{// soon}
functionpromisify<Argsextendsunknown[],Res>(fn:(...args:[...Args,(result:Res)=>void])=>void):(...args:Args)=>Promise<Res>{// soon}
promisify现内容如下:
promisify now reads:
有两个泛型类型参数:Args,它需要是一个数组(或元组),和Res。
There are two generic type parameters: Args, which needs to be an array (or tuple), and Res.
的参数promisify是一个函数,其第一个参数是的元素Args,最后一个参数是一个具有类型参数的函数Res。
The parameter of promisify is a function where the first arguments are the elements of Args and the last argument is a function with a parameter of type Res.
promisify返回一个接受参数的函数Args并返回Promise的Res。
promisify returns a function that takes Args for parameters and returns a Promise of Res.
如果您尝试新的类型promisify,您会发现我们得到了我们想要的类型。
If you try out the new typings for promisify, you can see that we get exactly the type we want.
但它变得更好了。如果你看一下函数签名,你就会非常清楚我们期望哪些参数,即使它们是可变的并且将被替换为实数类型。我们可以使用相同的类型来实现promisify:
But it gets even better. If you look at the function signature, it’s absolutely clear which arguments we expect, even if they are variadic and will be substituted with real types. We can use the same types for the implementation of promisify:
functionpromisify<Argsextendsunknown[],Res>(fn:(...args:[...Args,(result:Res)=>void])=>void):(...args:Args)=>Promise<Res>{returnfunction(...args:Args){returnnewPromise((resolve)=>{functioncallback(res:Res){resolve(res);}fn.call(null,...[...args,callback]);});};}
functionpromisify<Argsextendsunknown[],Res>(fn:(...args:[...Args,(result:Res)=>void])=>void):(...args:Args)=>Promise<Res>{returnfunction(...args:Args){returnnewPromise((resolve)=>{functioncallback(res:Res){resolve(res);}fn.call(null,...[...args,callback]);});};}
那么它有什么作用呢?
So what does it do?
我们返回一个接受除回调之外的所有参数的函数。
We return a function that accepts all parameters except for the callback.
此函数返回一个新创建的Promise。
This function returns a newly created Promise.
由于我们还没有回调,因此我们需要构造它。它做什么?它resolve从 调用函数Promise,产生结果。
Since we don’t have a callback yet, we need to construct it. What does it do? It calls the resolve function from the Promise, producing a result.
被分割的部分需要重新组合起来!我们将回调添加到参数中并调用原始函数。
What has been split needs to be brought back together! We add the callback to the arguments and call the original function.
就是这样。一个promisify遵循回调模式的函数的工作函数。类型完美。我们甚至保留了参数名称。
And that’s it. A working promisify function for functions that adhere to the callback pattern. Perfectly typed. And we even keep the parameter names.
将条件类型与可变元组类型结合起来,始终删去第一个参数。
Combine conditional types with variadic tuple types, always shaving off the first parameter.
柯里化是函数式编程中非常著名的技术。柯里化将接受多个参数的函数转换为每个接受单个参数的函数序列。
Currying is a very well-known technique in functional programming. Currying converts a function that takes several arguments into a sequence of functions that each takes a single argument.
底层概念称为“函数参数的部分应用”。我们使用它来最大化函数的重用。柯里化的“Hello, World!”实现了一个add函数,该函数稍后可以部分应用第二个参数:
The underlying concept is called “partial application of function arguments.” We use it to maximize the reuse of functions. The “Hello, World!” of currying implements an add function that can partially apply the second argument later:
functionadd(a:number,b:number){returna+b;}constcurriedAdd=curry(add);// convert: (a: number) => (b: number) => numberconstadd5=curriedAdd(5);// apply first argument. (b: number) => numberconstresult1=add5(2);// second argument. Result: 7constresult2=add5(3);// second argument. Result: 8
functionadd(a:number,b:number){returna+b;}constcurriedAdd=curry(add);// convert: (a: number) => (b: number) => numberconstadd5=curriedAdd(5);// apply first argument. (b: number) => numberconstresult1=add5(2);// second argument. Result: 7constresult2=add5(3);// second argument. Result: 8
乍一看似乎很随意的东西在处理长参数列表时很有用。以下通用函数可以向 中添加或删除类HTMLElement。
What feels arbitrary at first is useful when you work with long argument lists. The following generalized function either adds or removes classes to an HTMLElement.
除了最后的活动,我们可以准备好一切:
We can prepare everything except for the final event:
functionapplyClass(this:HTMLElement,// for TypeScript onlymethod:"remove"|"add",className:string,event:Event){if(this===event.target){this.classList[method](className);}}constapplyClassCurried=curry(applyClass);// convertconstremoveToggle=applyClassCurried("remove")("hidden");document.querySelector(".toggle")?.addEventListener("click",removeToggle);
functionapplyClass(this:HTMLElement,// for TypeScript onlymethod:"remove"|"add",className:string,event:Event){if(this===event.target){this.classList[method](className);}}constapplyClassCurried=curry(applyClass);// convertconstremoveToggle=applyClassCurried("remove")("hidden");document.querySelector(".toggle")?.addEventListener("click",removeToggle);
这样,我们可以removeToggle在多个元素上重复使用多个事件。我们还可以将其applyClass用于许多其他情况。
This way, we can reuse removeToggle for several events on several elements. We can also use applyClass for many other situations.
柯里化是编程语言 Haskell 的一个基本概念,它向数学家 Haskell Brooks Curry 致敬,编程语言和该技术都以他的名字命名。在 Haskell 中,每个操作都是柯里化的,程序员可以很好地利用它。
Currying is a fundamental concept of the programming language Haskell and gives a nod to the mathematician Haskell Brooks Curry, the namesake for both the programming language and the technique. In Haskell, every operation is curried, and programmers make good use of it.
JavaScript 大量借鉴了函数式编程语言,并且可以使用其内置的绑定功能来实现部分应用:
JavaScript borrows heavily from functional programming languages, and it is possible to implement partial application with its built-in functionality of binding:
functionadd(a:number,b:number,c:number){returna+b+c;}// Partial applicationconstpartialAdd5And3=add.bind(this,5,3);constresult=partialAdd5And3(2);// third argument
functionadd(a:number,b:number,c:number){returna+b+c;}// Partial applicationconstpartialAdd5And3=add.bind(this,5,3);constresult=partialAdd5And3(2);// third argument
由于函数是 JavaScript 中的一等公民,我们可以创建一个curry函数,该函数以一个函数作为参数,并在执行之前收集所有参数:
Since functions are first-class citizens in JavaScript, we can create a curry function that takes a function as an argument and collects all arguments before executing it:
functioncurry(fn){letcurried=(...args)=>{// if you haven't collected enough argumentsif(fn.length!==args.length){// partially apply arguments and// return the collector functionreturncurried.bind(null,...args);}// otherwise call all functionsreturnfn(...args);};returncurried;}
functioncurry(fn){letcurried=(...args)=>{// if you haven't collected enough argumentsif(fn.length!==args.length){// partially apply arguments and// return the collector functionreturncurried.bind(null,...args);}// otherwise call all functionsreturnfn(...args);};returncurried;}
诀窍在于每个函数都会将定义参数的数量存储在其length属性中。这就是我们可以在将所有必要参数应用于传递的函数之前递归收集它们的方法。
The trick is that every function stores the number of defined arguments in its length property. That’s how we can recursively collect all necessary arguments before applying them to the function passed.
那么缺少什么?类型!让我们创建一个适用于柯里化模式的类型,其中每个序列化函数只能接受一个参数。我们通过创建一个条件类型来实现这一点,该条件类型执行curried函数内部函数的逆操作curry:删除参数。
So what’s missing? Types! Let’s create a type that works for a currying pattern where every sequenced function can take exactly one argument. We do this by creating a conditional type that does the inverse of what the curried function inside the curry function does: removing arguments.
那么让我们创建一个Curried<F>类型。首先要检查该类型是否确实是
一个函数:
So let’s create a Curried<F> type. The first thing is to check if the type is indeed
a function:
typeCurried<F>=Fextends(...args:inferA)=>inferR?/* to be done */:never;// not a function, this should not happen
typeCurried<F>=Fextends(...args:inferA)=>inferR?/* to be done */:never;// not a function, this should not happen
我们还将参数推断为A并将返回类型推断为R。下一步,我们将第一个参数削减为F,并将所有剩余参数存储在L(最后一个) 中:
We also infer the arguments as A and the return type as R. Next step, we shave off the first parameter as F, and store all remaining parameters in L (for last):
typeCurried<F>=Fextends(...args:inferA)=>inferR?Aextends[inferF,...inferL]?/* to be done */:()=>R:never;
typeCurried<F>=Fextends(...args:inferA)=>inferR?Aextends[inferF,...inferL]?/* to be done */:()=>R:never;
如果没有参数,我们将返回一个不带参数的函数。最后检查:我们检查剩余参数是否为空。这意味着我们已经完成了从参数列表中删除参数的工作:
Should there be no arguments, we return a function that takes no arguments. Last check: we check if the remaining parameters are empty. This means we reached the end of removing arguments from the argument list:
typeCurried<F>=Fextends(...args:inferA)=>inferR?Aextends[inferF,...inferL]?Lextends[]?(a:F)=>R:(a:F)=>Curried<(...args:L)=>R>:()=>R:never;
typeCurried<F>=Fextends(...args:inferA)=>inferR?Aextends[inferF,...inferL]?Lextends[]?(a:F)=>R:(a:F)=>Curried<(...args:L)=>R>:()=>R:never;
如果仍有一些参数,我们会Curried再次调用该类型,但使用剩余的参数。这样,我们一步步地削减参数,如果你仔细观察,就会发现这个过程与我们在函数中所做的几乎相同
curried。我们在 中解构参数Curried<F>,然后在 中再次收集它们curried(fn)。
Should some parameters remain, we call the Curried type again, but with the remaining parameters. This way, we shave off a parameter step by step, and if you take a good look, you can see that the process is almost identical to what we do in the
curried function. Where we deconstruct parameters in Curried<F>, we collect them again in curried(fn).
类型完成后,我们将其添加到curry:
With the type done, let’s add it to curry:
functioncurry<FextendsFunction>(fn:F):Curried<F>{letcurried:Function=(...args:any)=>{if(fn.length!==args.length){returncurried.bind(null,...args);}returnfn(...args);};returncurriedasCurried<F>;}
functioncurry<FextendsFunction>(fn:F):Curried<F>{letcurried:Function=(...args:any)=>{if(fn.length!==args.length){returncurried.bind(null,...args);}returnfn(...args);};returncurriedasCurried<F>;}
any由于类型的灵活性,我们需要一些断言和一些其他断言。但是使用as和any作为关键字,我们标记哪些部分被视为不安全类型。
We need a few assertions and some any because of the flexible nature of the type. But with as and any as keywords, we mark which portions are considered unsafe types.
方案 7.3curry中的函数允许传递任意数量的参数,但是您的输入允许您一次只接受一个参数。
The curry function from Recipe 7.3 allows for an arbitrary number of arguments to be passed, but your typings allow you to take only one argument at a time.
扩展您的类型来为所有可能的元组组合创建函数重载。
Extend your typings to create function overloads for all possible tuple combinations.
在7.3 节中,我们得到了允许我们一次只应用一个函数参数的函数类型:
In Recipe 7.3 we ended up with function types that allow us to apply function arguments one at a time:
functionaddThree(a:number,b:number,c:number){returna+b+c;}constadder=curried(addThree);constadd7=adder(5)(2);constresult=add7(2);
functionaddThree(a:number,b:number,c:number){returna+b+c;}constadder=curried(addThree);constadd7=adder(5)(2);constresult=add7(2);
但是,curry函数本身可以采用任意参数列表:
However, the curry function itself can take an arbitrary list of arguments:
functionaddThree(a:number,b:number,c:number){returna+b+c;}constadder=curried(addThree);constadd7=adder(5,2);// this is the differenceconstresult=add7(2);
functionaddThree(a:number,b:number,c:number){returna+b+c;}constadder=curried(addThree);constadd7=adder(5,2);// this is the differenceconstresult=add7(2);
这使我们能够处理相同的用例,但函数调用次数要少得多。因此,让我们调整类型以充分利用体验curry。
This allows us to work on the same use cases but with a lot fewer function invocations. So let’s adapt our types to take advantage of the full curry experience.
此示例很好地说明了类型系统如何作为 JavaScript 上的薄层工作。通过any在正确的位置添加断言和,我们可以有效地定义curry应该如何工作,而函数本身则更加灵活。请注意,当您在复杂功能之上定义复杂类型时,您可能会欺骗自己以达到目标,而类型最终如何工作完全由您决定。请相应地进行测试。
This example illustrates really well how the type system works as just a thin layer on top of JavaScript. By adding assertions and any at the right positions, we effectively define how curry should work, whereas the function itself is much more flexible. Be aware that when you define complex types on top of complex functionality, you might cheat your way to the goal, and it’s in your hands how the types work in the end. Test accordingly.
我们的目标是创建一个可以为每个部分应用生成所有可能的函数签名的类型。对于该addThree函数,所有可能的类型如下所示:
Our goal is to create a type that can produce all possible function signatures for every partial application. For the addThree function, all possible types would look like this:
typeAdder=(a:number)=>(b:number)=>(c:number)=>number;typeAdder=(a:number)=>(b:number,c:number)=>number;typeAdder=(a:number,b:number)=>(c:number)=>number;typeAdder=(a:number,b:number,c:number)=>number;
typeAdder=(a:number)=>(b:number)=>(c:number)=>number;typeAdder=(a:number)=>(b:number,c:number)=>number;typeAdder=(a:number,b:number)=>(c:number)=>number;typeAdder=(a:number,b:number,c:number)=>number;
另请参见图 7-1以了解所有可能的调用图的可视化。
See also Figure 7-1 for a visualization of all possible call graphs.
addThree;有三个分支开始,可能还有第四个分支我们要做的第一件事是稍微调整一下调用辅助类型的方式。在原始类型中,我们在辅助类型中Curried推断函数参数和返回类型。现在我们需要在多个类型调用中携带返回值,因此我们直接在函数中提取返回类型和参数:curry
The first thing we do is to slightly adapt the way we call the Curried helper type. In the original type, we do the inference of function arguments and return types in the helper type. Now we need to carry along the return value over multiple type invocations, so we extract the return type and arguments directly in the curry function:
functioncurry<Aextendsany[],Rextendsany>(fn:(...args:A)=>R):Curried<A,R>{// see before, we're not changing the implementation}
functioncurry<Aextendsany[],Rextendsany>(fn:(...args:A)=>R):Curried<A,R>{// see before, we're not changing the implementation}
接下来,我们重新定义Curried类型。它现在具有两个泛型类型参数:A用于参数,R用于返回类型。第一步,我们检查参数是否包含元组元素。我们提取第一个元素F和所有剩余元素L。如果没有剩余元素,我们返回返回类型R:
Next, we redefine the Curried type. It now features two generic type parameters: A for arguments, R for the return type. As a first step, we check if the arguments contain tuple elements. We extract the first element F and all remaining elements L. If there are no elements left, we return the return type R:
typeCurried<Aextendsany[],Rextendsany>=Aextends[inferF,...inferL]?// to be done:R;
typeCurried<Aextendsany[],Rextendsany>=Aextends[inferF,...inferL]?// to be done:R;
无法通过 rest 运算符提取多个元组。这就是为什么我们仍然需要删除第一个元素并收集剩余元素的原因L。但没关系;我们至少需要一个参数才能有效地进行部分应用。
It’s not possible to extract multiple tuples via the rest operator. That’s why we still need to shave off the first element and collect the remaining elements in L. But that’s OK; we need at least one parameter to effectively do partial application.
当我们处于分支中时true,我们创建函数定义。在上一个示例中,我们返回了一个返回递归调用的函数;现在我们需要提供所有可能的部分应用。
When we are in the true branch, we create the function definitions. In the previous example, we returned a function that returns a recursive call; now we need to provide all possible partial applications.
由于函数参数只是元组类型(参见7.2 节),函数重载的参数可以描述为元组类型的并集。类型Overloads接受一个函数参数元组并创建所有偏应用:
Since function arguments are nothing but tuple types (see Recipe 7.2), arguments of function overloads can be described as a union of tuple types. A type Overloads takes a tuple of function arguments and creates all partial applications:
typeOverloads<Aextendsany[]>=Aextends[inferA,...inferL]?[A]|[A,...Overloads<L>]|[]:[];
typeOverloads<Aextendsany[]>=Aextends[inferA,...inferL]?[A]|[A,...Overloads<L>]|[]:[];
如果我们传递一个元组,我们将得到一个从空元组开始的并集,然后增长到一个参数,然后是两个参数等等,直到包含所有 参数的元组:
If we pass a tuple, we get a union starting from the empty tuple and then growing to one argument, then to two arguments, etc., and up to a tuple that includes all arguments:
// type Overloaded = [] | [string, number, string] | [string] | [string, number]typeOverloaded=Overloads<[string,number,string]>;
// type Overloaded = [] | [string, number, string] | [string] | [string, number]typeOverloaded=Overloads<[string,number,string]>;
现在我们可以定义所有重载,我们采用原始函数参数列表中的剩余参数并创建所有可能的函数调用,其中也包括第一个参数:
Now that we can define all overloads, we take the remaining arguments of the original functions’ argument list and create all possible function calls that also include the first argument:
typeCurried<Aextendsany[],Rextendsany>=Aextends[inferF,...inferL]?<KextendsOverloads<L>>(arg:F,...args:K)=>/* to be done */:R;
typeCurried<Aextendsany[],Rextendsany>=Aextends[inferF,...inferL]?<KextendsOverloads<L>>(arg:F,...args:K)=>/* to be done */:R;
应用于addThree之前的例子,这部分将创建第一个参数F为number,然后将其与[]、[number]和结合起来[number, number]。
Applied to the addThree example from before, this part would create the first argument F as number and then combine it with [], [number], and [number, number].
现在来看看返回类型。这又是一个对 的递归调用Curried,就像在7.2 节中一样。记住,我们按顺序链接函数。我们传入相同的返回类型——我们最终需要到达那里——但还需要传递我们尚未在函数重载中分散的所有剩余参数。因此,如果我们addThree只使用
调用number,则剩余的两个数字需要作为 的下一次迭代的参数
Curried。这就是我们创建可能调用树的方式。
Now for the return type. This is again a recursive call to Curried, just like in Recipe 7.2. Remember, we chain functions in a sequence. We pass in the same return type—we need to get there eventually—but also need to pass all remaining arguments that we haven’t spread out in the function overloads. So if we call addThree only with
number, the two remaining numbers need to be arguments of the next iteration of
Curried. This is how we create a tree of possible invocations.
为了得到可能的组合,我们需要从剩余的参数中删除函数签名中已经描述的参数。辅助类型Remove<T, U>遍历两个元组并分别删除一个元素,直到两个元组中有一个元组用尽元素:
To get to the possible combinations, we need to remove the arguments we already described in the function signature from the remaining arguments. A helper type Remove<T, U> goes through both tuples and shaves off one element each, until one of the two tuples runs out of elements:
typeRemove<Textendsany[],Uextendsany[]>=Uextends[infer_,...inferUL]?Textends[infer_,...inferTL]?Remove<TL,UL>:never:T;
typeRemove<Textendsany[],Uextendsany[]>=Uextends[infer_,...inferUL]?Textends[infer_,...inferTL]?Remove<TL,UL>:never:T;
将其连接到Curried,我们得到最终结果:
Wiring that up to Curried, and we get the final result:
typeCurried<Aextendsany[],Rextendsany>=Aextends[inferF,...inferL]?<KextendsOverloads<L>>(arg:F,...args:K)=>Curried<Remove<L,K>,R>:R;
typeCurried<Aextendsany[],Rextendsany>=Aextends[inferF,...inferL]?<KextendsOverloads<L>>(arg:F,...args:K)=>Curried<Remove<L,K>,R>:R;
Curried<A, R>现在生成与图 7-1中所述相同的调用图,但对于我们传入的所有可能的函数都是灵活的curry。适当的类型安全性可实现最大的灵活性(感谢 GitHub 用户 Akira Matsuzaki,他提供了
Type Challenges 解决方案中缺失的部分)。
Curried<A, R> now produces the same call graph as described in Figure 7-1 but is flexible for all possible functions that we pass in curry. Proper type safety for maximum flexibility (shout-out to GitHub user Akira Matsuzaki who provided the
missing piece in their Type Challenges solution).
仅需一个连续步骤即可创建curry函数。TypeScript 可以自行确定正确的类型。
Create a curry function with only a single sequential step. TypeScript can figure out the proper types on its own.
在三部曲的最后一部分curry,我希望您坐下来思考一下我们在配方7.3和7.4中看到的内容。我们通过 TypeScript 的元编程功能创建了非常复杂的类型,其工作方式几乎与实际实现一样。虽然结果令人印象深刻,但我们需要考虑一些注意事项:
In the last piece of the curry trilogy, I want you to sit back and think a bit about what we saw in Recipes 7.3 and 7.4. We created very complex types that work almost like the actual implementation through TypeScript’s metaprogramming features. And while the results are impressive, there are some caveats we have to think about:
7.3 节和7.4节的类型实现方式略有不同,但结果却大不相同!不过,curry底层函数保持不变。唯一可行的方法是使用any参数和类型断言作为返回类型。这意味着我们通过强制 TypeScript 遵循我们的世界观来有效地禁用类型检查。TypeScript 可以做到这一点很棒,有时这也是必要的(例如创建新对象),但它可能会适得其反,尤其是当实现和类型都非常复杂时。类型和实现的测试都是必须的。我们将在12.4 节中讨论测试类型。
The way the types are implemented for both Recipes 7.3 and 7.4 is a bit different, but the results vary a lot! Still, the curry function underneath stays the same. The only way this works is by using any in arguments and type assertions for the return type. What this means is that we effectively disable type-checking by forcing TypeScript to adhere to our view of the world. It’s great that TypeScript can do that, and at times it’s also necessary (such as the creation of new objects), but it can backfire, especially when both implementation and types get very complex. Tests for both types and implementation are a must. We talk about testing types in Recipe 12.4.
您会丢失信息。尤其是在柯里化时,保留参数名称对于了解哪些参数已经应用至关重要。前面的配方中的解决方案无法保留参数名称,而是默认为通用的a或args。如果您的参数类型是所有字符串,则无法说出您当前正在编写哪个字符串。
You lose information. Especially when currying, keeping argument names is essential to know which arguments already have applied. The solutions in the earlier recipes couldn’t keep argument names but defaulted to a generic-sounding a or args. If your argument types are, for example, all strings, you can’t say which string you are currently writing.
虽然方案 7.4中的结果为您提供了正确的类型检查,但由于类型的性质,自动完成功能受到限制。您只知道在键入第二个参数时需要它。TypeScript 的主要功能之一是为您提供正确的工具和信息,以提高您
Curried的工作效率。灵活的类型可让您再次依靠猜测来提高工作效率。
While the result in Recipe 7.4 gives you proper type-checking, autocomplete is limited because of the nature of the type. You know only that a second argument is needed the moment you type it. One of TypeScript’s main features is giving you the right tooling and information to make you more productive. The flexible
Curried type reduces your productivity to guesswork again.
再次,虽然这些类型令人印象深刻,但不可否认的是,它们会带来一些巨大的权衡。这就提出了一个问题:我们是否应该这样做?我认为这真的取决于你想要实现什么。
Again, while those types are impressive, there is no denying that they come with some huge trade-offs. This raises the question: should we even go for it? I think it really depends on what you try to achieve.
就柯里化和部分应用而言,有两个阵营。第一个阵营喜欢函数式编程模式,并试图最大限度地利用 JavaScript 的函数功能。他们希望尽可能多地重用部分应用,并且需要高级柯里化功能。另一个阵营在某些情况下看到了函数式编程模式的好处——例如,等待最后一个参数为多个事件提供相同的功能。他们通常乐于尽可能多地应用,然后在第二步提供其余部分。
In the case of currying and partial application, there are two camps. The first camp loves functional programming patterns and tries to leverage JavaScript’s functional capabilities to the max. They want to reuse partial applications as much as possible and need advanced currying functionalities. The other camp sees the benefit of functional programming patterns in certain situations—for example, waiting for the final parameter to give the same function to multiple events. They often are happy with applying as much as possible, but then provide the rest in a second step.
到目前为止,我们只处理了第一种阵营。如果你属于第二种阵营,你很可能只需要一个部分应用一些参数的柯里化函数,这样你就可以在第二个步骤中传入其余参数:没有一个参数的参数序列,也没有灵活应用任意数量的参数。理想的界面应该是这样的:
We have dealt with only the first camp until now. If you’re in the second camp, you most likely only need a currying function that applies a few parameters partially, so you can pass in the rest in a second step: no sequence of parameters of one argument, and no flexible application of as many arguments as you like. An ideal interface would look like this:
functionapplyClass(this:HTMLElement,// for TypeScript onlymethod:"remove"|"add",className:string,event:Event){if(this===event.target){this.classList[method](className);}}constremoveToggle=curry(applyClass,"remove","hidden");document.querySelector("button")?.addEventListener("click",removeToggle);
functionapplyClass(this:HTMLElement,// for TypeScript onlymethod:"remove"|"add",className:string,event:Event){if(this===event.target){this.classList[method](className);}}constremoveToggle=curry(applyClass,"remove","hidden");document.querySelector("button")?.addEventListener("click",removeToggle);
curry是一个函数,它接受另一个函数f作为参数,然后接受一系列t的参数f。它返回一个接受其余参数u的函数f,该函数f使用所有可能的参数进行调用。该函数在 JavaScript 中可能如下所示
:
curry is a function that takes another function f as an argument and then a sequence t of parameters of f. It returns a function that takes the remaining parameters u of f, which calls f with all possible parameters. The function could look like this in
JavaScript:
functioncurry(f,...t){return(...u)=>f(...t,...u);}
functioncurry(f,...t){return(...u)=>f(...t,...u);}
由于 rest 和 spread 运算符,curry它变成了一行代码。现在让我们输入这个!我们将不得不使用泛型,因为我们要处理尚不知道的参数。有返回类型R,以及函数参数的两个部分,T和U。后者是可变元组类型,需要这样定义。
Thanks to the rest and spread operator, curry becomes a one-liner. Now let’s type this! We will have to use generics, as we deal with parameters that we don’t know yet. There’s the return type R, as well as both parts of the function’s arguments, T and U. The latter are variadic tuple types and need to be defined as such.
使用泛型类型参数T并U包含的参数f,的类型f如下所示:
With a generic type parameter T and U comprising the arguments of f, a type for f looks like this:
typeFn<Textendsany[],Uextendsany[]>=(...args:[...T,...U])=>any;
typeFn<Textendsany[],Uextendsany[]>=(...args:[...T,...U])=>any;
函数参数可以描述为元组,这里我们说这些函数参数应该分成两部分。让我们内联此类型curry并使用另一个泛型类型参数作为返回类型R:
Function arguments can be described as tuples, and here we say those function arguments should be split into two parts. Let’s inline this type to curry and use another generic type parameter for the return type R:
functioncurry<Textendsany[],Uextendsany[],R>(f:(...args:[...T,...U])=>R,...t:T){return(...u:U)=>f(...t,...u);}
functioncurry<Textendsany[],Uextendsany[],R>(f:(...args:[...T,...U])=>R,...t:T){return(...u:U)=>f(...t,...u);}
这就是我们需要的所有类型:简单、直接,并且类型看起来与实际实现非常相似。通过一些可变元组类型,TypeScript 为我们提供:
And that’s all the types we need: simple, straightforward, and the types look very similar to the actual implementation. With a few variadic tuple types, TypeScript gives us:
100% 类型安全。TypeScript 直接根据您的使用情况推断出泛型类型,并且它们是正确的。无需通过条件类型和递归费力地制作类型。
100% type safety. TypeScript directly infers the generic types from your usage, and they are correct. No laboriously crafted types through conditional types and recursion.
我们为所有可能的解决方案提供自动完成功能。当您添加一个,来宣布参数的下一步时,TypeScript 将调整类型并提示您将要做什么。
We get autocomplete for all possible solutions. The moment you add a , to announce the next step of your arguments, TypeScript will adapt types and give you a hint about what to expect.
我们不会丢失任何信息。由于我们不构造新类型,TypeScript 会保留原始类型的标签,并且我们知道需要哪些参数。
We don’t lose any information. Since we don’t construct new types, TypeScript keeps the labels from the original type, and we know which arguments to expect.
是的,curry不如原始版本灵活,但对于很多用例来说,这可能是正确的选择。这完全取决于我们为用例接受的权衡。
Yes, curry is not as flexible as the original version, but for a lot of use cases, this might be the right choice. It’s all about the trade-offs we accept for our use case.
如果你经常使用元组,你可以将元组类型的元素命名为:type Person = [name: string, age: number];。这些标签只是注释,在
转译后会被删除。
If you work with tuples a lot, you can name the elements of your tuple types: type Person = [name: string, age: number];. Those labels are just annotations and are removed after
transpilation.
最终,该curry函数及其多种不同的实现代表了使用 TypeScript 解决特定问题的多种方式。您可以全力以赴地使用类型系统并将其用于非常复杂和精细的类型,也可以稍微缩小范围并让编译器为您完成工作。您的选择取决于您的目标和您想要实现的目标。
Ultimately, the curry function and its many different implementations stand for the many ways you can use TypeScript to solve a particular problem. You can go all out with the type system and use it for very complex and elaborate types, or you can reduce the scope a bit and let the compiler do the work for you. Your choice depends on your goals and what you try to achieve.
您喜欢使用枚举轻松选择有效值的方式,但是在阅读了方案 3.12之后,您不想处理它们的所有警告。
You like how enums make it easy to select valid values, but after reading Recipe 3.12 you don’t want to deal with all their caveats.
从元组创建枚举。使用条件类型、可变元组类型和"length"属性来输入数据结构。
Create your enums from a tuple. Use conditional types, variadic tuple types, and the "length" property to type the data structure.
在3.12 节中,我们讨论了使用数字和字符串枚举时可能出现的所有注意事项。我们最终采用了一种更接近类型系统的模式,但能提供与常规枚举相同的开发体验:
In Recipe 3.12 we discussed all possible caveats when using number and string enums. We ended up with a pattern that is much closer to the type system but gives you the same developer experience as regular enums:
constDirection={Up:0,Down:1,Left:2,Right:3,}asconst;// Get to the const values of DirectiontypeDirection=(typeofDirection)[keyoftypeofDirection];// (typeof Direction)[keyof typeof Direction] yields 0 | 1 | 2 | 3functionmove(direction:Direction){// tbd}move(30);// This breaks!move(0);//This works!move(Direction.Left);// This also works!
constDirection={Up:0,Down:1,Left:2,Right:3,}asconst;// Get to the const values of DirectiontypeDirection=(typeofDirection)[keyoftypeofDirection];// (typeof Direction)[keyof typeof Direction] yields 0 | 1 | 2 | 3functionmove(direction:Direction){// tbd}move(30);// This breaks!move(0);//This works!move(Direction.Left);// This also works!
这是一个非常简单的模式,没有什么意外,但是如果你要处理大量条目,它可能会给你带来很多工作,特别是当你想拥有字符串枚举时:
It’s a very straightforward pattern with no surprises, but it can result in a lot of work for you if you are dealing with lots of entries, especially if you want to have string enums:
constCommands={Shift:"shift",Xargs:"xargs",Tail:"tail",Head:"head",Uniq:"uniq",Cut:"cut",Awk:"awk",Sed:"sed",Grep:"grep",Echo:"echo",}asconst;
constCommands={Shift:"shift",Xargs:"xargs",Tail:"tail",Head:"head",Uniq:"uniq",Cut:"cut",Awk:"awk",Sed:"sed",Grep:"grep",Echo:"echo",}asconst;
存在重复,这可能会导致拼写错误,进而导致未定义的行为。为您创建此类枚举的辅助函数有助于处理冗余和重复。假设您有一组如下项目:
There is duplication, which may result in typos, which may lead to undefined behavior. A helper function that creates an enum like this for you helps deal with redundancy and duplication. Let’s say you have a collection of items like this:
constcommandItems=["echo","grep","sed","awk","cut","uniq","head","tail","xargs","shift",]asconst;
constcommandItems=["echo","grep","sed","awk","cut","uniq","head","tail","xargs","shift",]asconst;
辅助函数createEnum遍历每个项目,创建一个带有大写键的对象,该对象指向字符串值或数字值,具体取决于您的输入参数:
A helper function createEnum iterates through every item, creating an object with capitalized keys that point either to a string value or to a number value, depending on your input parameters:
functioncapitalize(x:string):string{returnx.charAt(0).toUpperCase()+x.slice(1);}// Typings to be donefunctioncreateEnum(arr,numeric){letobj={};for(let[i,el]ofarr.entries()){obj[capitalize(el)]=numeric?i:el;}returnobj;}constCommand=createEnum(commandItems);// string enumconstCommandN=createEnum(commandItems,true);// number enum
functioncapitalize(x:string):string{returnx.charAt(0).toUpperCase()+x.slice(1);}// Typings to be donefunctioncreateEnum(arr,numeric){letobj={};for(let[i,el]ofarr.entries()){obj[capitalize(el)]=numeric?i:el;}returnobj;}constCommand=createEnum(commandItems);// string enumconstCommandN=createEnum(commandItems,true);// number enum
让我们为此创建类型!我们需要注意两件事:
Let’s create types for this! We need to take care of two things:
从元组创建一个对象。键名大写。
Create an object from a tuple. The keys are capitalized.
将每个属性键的值设置为字符串值或数字值。数字值应从 0 开始,每一步增加 1。
Set the values of each property key to either a string value or a number value. The number values should start at 0 and increase by one with each step.
要创建对象键,我们需要一个可以映射的联合类型。要获取所有对象键,我们需要将元组转换为联合类型。辅助类型TupleToUnion接受字符串元组并将其转换为联合类型。为什么只有字符串元组?因为我们需要对象键,而字符串键最容易使用。
To create object keys, we need a union type we can map out. To get all object keys, we need to convert our tuple to a union type. A helper type TupleToUnion takes a string tuple and converts it to a union type. Why only string tuples? Because we need object keys, and string keys are the easiest to use.
TupleToUnion<T>是一种递归类型。就像我们在其他课程中所做的那样,我们正在删除单个元素(这次是在元组的末尾),然后使用剩余元素再次调用该类型。我们将每个调用放在一个联合中,有效地获得元组元素的联合类型:
TupleToUnion<T> is a recursive type. Like we did in other lessons, we are shaving off single elements—this time at the end of the tuple—and then calling the type again with the remaining elements. We put each call in a union, effectively getting a union type of tuple elements:
typeTupleToUnion<Textendsreadonlystring[]>=Textendsreadonly[...inferRestextendsstring[],inferKeyextendsstring]?Key|TupleToUnion<Rest>:never;
typeTupleToUnion<Textendsreadonlystring[]>=Textendsreadonly[...inferRestextendsstring[],inferKeyextendsstring]?Key|TupleToUnion<Rest>:never;
使用映射类型和字符串操作类型,我们可以创建字符串枚举版本Enum<T>:
With a map type and a string manipulation type, we can create the string enum version of Enum<T>:
typeEnum<Textendsreadonlystring[],Nextendsboolean=false>=Readonly<{[KinTupleToUnion<T>asCapitalize<K>]:K}>;
typeEnum<Textendsreadonlystring[],Nextendsboolean=false>=Readonly<{[KinTupleToUnion<T>asCapitalize<K>]:K}>;
对于数字枚举版本,我们需要获取每个值的数字表示。如果我们考虑一下,我们已经将它存储在原始数据的某个地方了。让我们看看如何TupleToUnion处理四元素元组:
For the number enum version, we need to get a numerical representation of each value. If we think about it, we have already stored it somewhere in our original data. Let’s look at how TupleToUnion deals with a four-element tuple:
// The type we want to convert to a union typetypeDirection=["up","down","left","right"];// Calling the helper typetypeDirectionUnion=TupleToUnion<Direction>;// Extracting the last, recursively calling TupleToUnion with the ResttypeDirectionUnion="right"|TupleToUnion<["up","down","left"]>;// Extracting the last, recursively calling TupleToUnion with the ResttypeDirectionUnion="right"|"left"|TupleToUnion<["up","down"]>;// Extracting the last, recursively calling TupleToUnion with the ResttypeDirectionUnion="right"|"left"|"down"|TupleToUnion<["up"]>;// Extracting the last, recursively calling TupleToUnion with an empty tupletypeDirectionUnion="right"|"left"|"down"|"up"|TupleToUnion<[]>;// The conditional type goes into the else branch, adding never to the uniontypeDirectionUnion="right"|"left"|"down"|"up"|never;// never in a union is swallowedtypeDirectionUnion="right"|"left"|"down"|"up";
// The type we want to convert to a union typetypeDirection=["up","down","left","right"];// Calling the helper typetypeDirectionUnion=TupleToUnion<Direction>;// Extracting the last, recursively calling TupleToUnion with the ResttypeDirectionUnion="right"|TupleToUnion<["up","down","left"]>;// Extracting the last, recursively calling TupleToUnion with the ResttypeDirectionUnion="right"|"left"|TupleToUnion<["up","down"]>;// Extracting the last, recursively calling TupleToUnion with the ResttypeDirectionUnion="right"|"left"|"down"|TupleToUnion<["up"]>;// Extracting the last, recursively calling TupleToUnion with an empty tupletypeDirectionUnion="right"|"left"|"down"|"up"|TupleToUnion<[]>;// The conditional type goes into the else branch, adding never to the uniontypeDirectionUnion="right"|"left"|"down"|"up"|never;// never in a union is swallowedtypeDirectionUnion="right"|"left"|"down"|"up";
如果仔细观察,您会发现元组的长度随着每次调用而减少。首先是三个元素,然后是两个,然后是一个,最后没有元素了。元组由数组的长度和数组中每个位置的类型定义。TypeScript 将元组的长度存储为数字,可通过以下"length"属性访问:
If you look closely, you can see that the length of the tuple is decreasing with each call. First, it’s three elements, then two, then one, and ultimately there are no elements left. Tuples are defined by the length of the array and the type at each position in the array. TypeScript stores the length as a number for tuples, accessible via the "length" property:
typeDirectionLength=Direction["length"];// 4
typeDirectionLength=Direction["length"];// 4
因此,每次递归调用时,我们都可以获取剩余元素的长度并将其用作枚举的值。我们不仅返回枚举键,还返回一个包含键及其可能的数字值的对象:
So with each recursive call, we can get the length of the remaining elements and use this as a value for the enum. Instead of just returning the enum keys, we return an object with the key and its possible number value:
typeTupleToUnion<Textendsreadonlystring[]>=Textendsreadonly[...inferRestextendsstring[],inferKeyextendsstring]?{key:Key;val:Rest["length"]}|TupleToUnion<Rest>:never;
typeTupleToUnion<Textendsreadonlystring[]>=Textendsreadonly[...inferRestextendsstring[],inferKeyextendsstring]?{key:Key;val:Rest["length"]}|TupleToUnion<Rest>:never;
我们使用这个新创建的对象来决定我们是否想要在枚举中使用数字值或字符串值:
We use this newly created object to decide whether we want to have number values or string values in our enum:
typeEnum<Textendsreadonlystring[],Nextendsboolean=false>=Readonly<{[KinTupleToUnion<T>asCapitalize<K["key"]>]:Nextendstrue?K["val"]:K["key"];}>;
typeEnum<Textendsreadonlystring[],Nextendsboolean=false>=Readonly<{[KinTupleToUnion<T>asCapitalize<K["key"]>]:Nextendstrue?K["val"]:K["key"];}>;
就这样!我们将新Enum<T, N>类型连接到createEnum函数:
And that’s it! We wire up our new Enum<T, N> type to the createEnum function:
typeValues<T>=T[keyofT];functioncreateEnum<Textendsreadonlystring[],Bextendsboolean>(arr:T,numeric?:B){letobj:any={};for(let[i,el]ofarr.entries()){obj[capitalize(el)]=numeric?i:el;}returnobjasEnum<T,B>;}constCommand=createEnum(commandItems,false);typeCommand=Values<typeofCommand>;
typeValues<T>=T[keyofT];functioncreateEnum<Textendsreadonlystring[],Bextendsboolean>(arr:T,numeric?:B){letobj:any={};for(let[i,el]ofarr.entries()){obj[capitalize(el)]=numeric?i:el;}returnobjasEnum<T,B>;}constCommand=createEnum(commandItems,false);typeCommand=Values<typeofCommand>;
能够在类型系统中访问元组的长度是 TypeScript 中的隐藏宝石之一。这允许实现很多功能(如本例所示),但也允许实现一些有趣的功能(如在类型系统中实现计算器)。与 TypeScript 中的所有高级功能一样,请明智地使用它们。
Being able to access the length of a tuple within the type system is one of the hidden gems in TypeScript. This allows for many things, as shown in this example, but also fun stuff like implementing calculators in the type system. As with all advanced features in TypeScript, use them wisely.
使用内置类型Parameters<F>和ReturnType<F>辅助类型。
Use the built-in Parameters<F> and ReturnType<F> helper types.
在本章中,我们讨论了辅助函数以及它们如何从作为参数的函数中获取信息。例如,此defer函数接受一个函数及其所有参数,并返回将执行它的另一个函数。使用一些泛型类型,我们可以捕获所需的一切:
In this chapter, we have dealt with helper functions and how they can grab information from functions that are arguments. For example, this defer function takes a function and all its arguments and returns another function that will execute it. With some generic types, we can capture everything we need:
functiondefer<Parextendsunknown[],Ret>(fn:(...par:Par)=>Ret,...args:Par):()=>Ret{return()=>fn(...args);}constlog=defer(console.log,"Hello, world!");log();
functiondefer<Parextendsunknown[],Ret>(fn:(...par:Par)=>Ret,...args:Par):()=>Ret{return()=>fn(...args);}constlog=defer(console.log,"Hello, world!");log();
如果我们将函数作为参数传递,效果会很好,因为我们可以轻松选择细节并重用它们。但某些场景需要函数的参数及其返回类型,而不是泛型函数。幸运的是,我们可以利用一些内置的 TypeScript 辅助类型。使用 ,Parameters<F>我们以元组形式获取函数的参数;使用 ,ReturnType<F>我们获取函数的返回类型。因此,defer之前的函数可以这样写:
This works great if we pass functions as arguments because we can easily pick the details and reuse them. But certain scenarios need a function’s arguments and its return type outside of a generic function. Thankfully, we can leverage some built-in TypeScript helper types. With Parameters<F> we get a function’s arguments as a tuple; with ReturnType<F> we get the return type of a function. So the defer function from before could be written like:
typeFn=(...args:any[])=>any;functiondefer<FextendsFn>(fn:F,...args:Parameters<F>):()=>ReturnType<F>{return()=>fn(...args);}
typeFn=(...args:any[])=>any;functiondefer<FextendsFn>(fn:F,...args:Parameters<F>):()=>ReturnType<F>{return()=>fn(...args);}
Parameters<F>和都是ReturnType<F>依赖于函数/元组类型的条件类型,非常相似。在 中Parameters<F>我们推断参数,在 中ReturnType<F>我们推断返回类型:
Both Parameters<F> and ReturnType<F> are conditional types that rely on function/tuple types and are very similar. In Parameters<F> we infer the arguments, and in ReturnType<F> we infer the return type:
typeParameters<Fextends(...args:any)=>any>=Fextends(...args:inferP)=>any?P:never;typeReturnType<Fextends(...args:any)=>any>=Fextends(...args:any)=>inferR?R:any;
typeParameters<Fextends(...args:any)=>any>=Fextends(...args:inferP)=>any?P:never;typeReturnType<Fextends(...args:any)=>any>=Fextends(...args:any)=>inferR?R:any;
例如,我们可以使用这些辅助类型在函数之外准备函数参数。以这个search函数为例:
We can use those helper types, for example, to prepare function arguments outside of functions. Take this search function:
typeResult={page:URL;title:string;description:string;};functionsearch(query:string,tags:string[]):Promise<Result[]>{throw"to be done";}
typeResult={page:URL;title:string;description:string;};functionsearch(query:string,tags:string[]):Promise<Result[]>{throw"to be done";}
我们知道了Parameters<typeof search>需要哪些参数。我们在函数调用之外定义它们,并在调用时将它们作为参数传播:
With Parameters<typeof search> we get an idea of which parameters to expect. We define them outside of the function call and spread them as arguments when calling:
constsearchParams:Parameters<typeofsearch>=["Variadic tuple tpyes",["TypeScript","JavaScript"],];search(...searchParams);constdeferredSearch=defer(search,...searchParams);
constsearchParams:Parameters<typeofsearch>=["Variadic tuple tpyes",["TypeScript","JavaScript"],];search(...searchParams);constdeferredSearch=defer(search,...searchParams);
当你生成新类型时,这两个助手都会派上用场;请参阅方案 4.8中的示例。
Both helpers come in handy when you generate new types as well; see Recipe 4.8 for an example.
TypeScript 的优势之一是能够从其他类型派生类型。这允许您定义类型之间的关系,其中一种类型的更新会自动传递到所有派生类型。这减少了维护工作,并最终实现更强大的类型设置。
One of TypeScript’s strengths is the ability to derive types from other types. This allows you to define relationships between types, where updates in one type trickle through to all derived types automatically. This reduces maintenance and ultimately results in more robust type setups.
创建派生类型时,我们通常会应用相同的类型修改,但组合不同。TypeScript 已经有一组内置的实用类型,其中一些我们已经在本书中见过。但有时它们还不够。有些情况下,你要么以不同的方式应用已知技术,要么深入挖掘类型系统的内部工作原理,以产生所需的结果。你可能需要一组自己的辅助类型。
When creating derived types, we usually apply the same type modifications but in different combinations. TypeScript already has a set of built-in utility types, some of which we’ve already seen in this book. But sometimes they are not enough. Some situations require you either to apply known techniques differently or to dig deep into the inner workings of the type system to produce the desired result. You might need your own set of helper types.
本章向您介绍了辅助类型的概念,并向您展示了一些用例,在这些用例中,自定义辅助类型极大地扩展了您从其他类型派生类型的能力。每种类型都设计用于不同的情况,并且每种类型都应该教您类型系统的一个新方面。当然,您在此处看到的类型列表绝不是完整的,但它们为您提供了一个良好的切入点和足够的资源来扩展。
This chapter introduces you to the concept of helper types and shows you some use cases where a custom helper type expands your ability to derive types from others tremendously. Each type is designed to work in different situations, and each type should teach you a new aspect of the type system. Of course, the list of types you see here is by no means complete, but they give you a good entry point and enough resources to branch out.
最后,TypeScript 的类型系统可以看作是它自己的函数式元编程语言,在其中,您可以将小型、单一用途的辅助类型与更大的辅助类型相结合,以使类型派生变得像将单一类型应用于现有模型一样简单。
In the end, TypeScript’s type system can be seen as its own functional meta-programming language, where you combine small, single-purpose helper types with bigger helper types to make type derivates as easy as applying a single type to your existing models.
您的 TypeScript 项目中的所有模型都已设置和定义,并且您想在整个代码中引用它们:
All your models in your TypeScript project are set and defined, and you want to refer to them throughout your code:
typePerson={name:string;age:number;profession:string;};
typePerson={name:string;age:number;profession:string;};
经常发生的一种情况是,你需要一些看起来像
Person但不需要设置所有属性的东西;其中一些可以是可选的。这将使你的 API 对其他结构和类型更加开放,这些结构和类型具有相似的形状,但缺少一两个字段。你不想维护不同的类型(参见配方 12.1),而是从仍在使用的原始模型中派生它们。
One situation that occurs pretty often is that you need something that looks like
Person but does not require all properties to be set; some of them can be optional. This will make your API more open to other structures and types that are of similar shape but lack one or two fields. You don’t want to maintain different types (see Recipe 12.1) but rather derive them from the original model, which is still in use.
TypeScript 有一个内置的辅助类型Partial<T>,可以将所有属性修改为可选:
TypeScript has a built-in helper type called Partial<T> that modifies all properties to be optional:
typePartial<T>={[PinkeyofT]?:T[P];};
typePartial<T>={[PinkeyofT]?:T[P];};
它是一种映射类型,可映射所有键,并使用可选映射类型修饰符将每个属性设置为可选。创建类型的第一步SetOptional是减少可以设置为可选的键集:
It’s a mapped type that maps out over all keys and uses the optional mapped type modifier to set each property to optional. The first step in making a SetOptional type is to reduce the set of keys that can be set as optional:
typeSelectPartial<T,KextendskeyofT>={[PinK]?:T[P]};
typeSelectPartial<T,KextendskeyofT>={[PinK]?:T[P]};
可选的映射类型修饰符将表示可选属性的符号(问号)应用于一组属性。您在4.5 节中学习了映射类型修饰符。
The optional mapped type modifier applies the symbol for an optional property—the question mark—to a set of properties. You learned about mapped type modifiers in Recipe 4.5.
在 中SelectPartial<T, K extends keyof T>,我们不会映射所有键,而只会映射提供的键子集。通过extends keyof T泛型约束,我们确保只传递有效的属性键。如果我们将 应用于SelectPartialselect Person,"age"我们最终会得到一个类型,其中我们只age看到设置
为可选的属性:
In SelectPartial<T, K extends keyof T>, we don’t map over all keys, just a subset of keys provided. With the extends keyof T generic constraint, we make sure that we pass only valid property keys. If we apply SelectPartial to Person to select "age", we end up with a type where we see only the age property, which is set
to optional:
typeAge=SelectPartial<Person,"age">;// type Age = { age?: number | undefined };
typeAge=SelectPartial<Person,"age">;// type Age = { age?: number | undefined };
前半部分已经完成:我们想要设置为可选的所有内容都是可选的。但缺少其余属性。让我们将它们恢复为对象类型。
The first half is done: everything we want to set as optional is optional. But the rest of the properties are missing. Let’s get them back to the object type.
用更多属性扩展现有对象类型的最简单方法是创建与另一个对象类型的交集类型。因此,在我们的例子中,我们采用我们写入的内容SelectPartial并将其与包含所有剩余键的类型相交。
The easiest way of extending an existing object type with more properties is to create an intersection type with another object type. So in our case, we take what we’ve written in SelectPartial and intersect it with a type that includes all remaining keys.
我们可以使用Exclude辅助类型获取所有剩余的键。Exclude<T, U>是比较两个集合的条件类型T。 如果集合中的元素在 中U,则使用 删除它们never;否则,它们保留在类型中:
We can get all remaining keys by using the Exclude helper type. Exclude<T, U> is a conditional type that compares two sets. If elements from set T are in U, they will be removed using never; otherwise, they stay in the type:
typeExclude<T,U>=TextendsU?never:T;
typeExclude<T,U>=TextendsU?never:T;
Extract<T, U>这与我们在5.3 节中描述的方法相反。Exclude<T, U>它是一种分配条件类型(参见5.2 节),并将条件类型分配给联合的每个元素:
This works in contrast to Extract<T, U> which we described in Recipe 5.3. Exclude<T, U> is a distributive conditional type (see Recipe 5.2) and distributes the conditional type over every element of a union:
// This example shows how TypeScript evaluates a// helper type step by step.typeExcludeAge=Exclude<"name"|"age","age">;// 1. DistributetypeExcludeAge="name"extends"age"?never:"name"|"age"extends"age"?never:"age";// 2. EvaluatetypeExcludeAge="name"|never;// 3. Remove unnecessary `never`typeExcludeAge="name";
// This example shows how TypeScript evaluates a// helper type step by step.typeExcludeAge=Exclude<"name"|"age","age">;// 1. DistributetypeExcludeAge="name"extends"age"?never:"name"|"age"extends"age"?never:"age";// 2. EvaluatetypeExcludeAge="name"|never;// 3. Remove unnecessary `never`typeExcludeAge="name";
这正是我们想要的!在 中SetOptional,我们创建一种类型,选择所有选定的键并使其变为可选,然后从所有对象键的更大集合中排除相同的键:
This is exactly what we want! In SetOptional, we create one type that picks all selected keys and makes them optional, then we exclude the same keys from the bigger set of all of the object’s keys:
typeSetOptional<T,KextendskeyofT>={[PinK]?:T[P];}&{[PinExclude<keyofT,K>]:T[P];};
typeSetOptional<T,KextendskeyofT>={[PinK]?:T[P];}&{[PinExclude<keyofT,K>]:T[P];};
两种类型的交集就是新的对象类型,我们可以将它与我们喜欢的任何模型一起使用:
The intersection of both types is the new object type, which we can use with any model we like:
typeOptionalAge=SetOptional<Person,"age">;/*type OptionalAge = {name: string;age?: number | undefined;profession: string;};*/
typeOptionalAge=SetOptional<Person,"age">;/*type OptionalAge = {name: string;age?: number | undefined;profession: string;};*/
如果我们想使多个键成为可选的,我们需要提供一个包含所有所需属性键的联合类型:
If we want to make more than one key optional, we need to provide a union type with all desired property keys:
typeOptionalAgeAndProf=SetOptional<Person,"age"|"profession">;
typeOptionalAgeAndProf=SetOptional<Person,"age"|"profession">;
TypeScript 不仅允许您自己定义这样的类型,而且还具有一组内置的辅助类型,您可以轻松组合它们以实现类似的效果。我们可以SetOptional仅基于辅助类型编写相同的类型:
TypeScript not only allows you to define types like this yourself but also has a set of built-in helper types that you can easily combine for similar effect. We could write the same type SetOptional solely based on helper types:
typeSetOptional<T,KextendskeyofT>=Partial<Pick<T,K>>&Omit<T,K>;
typeSetOptional<T,KextendskeyofT>=Partial<Pick<T,K>>&Omit<T,K>;
Pick<T, K>K从对象中选择键T。
Pick<T, K> selects keys K from object T.
Omit<T, K>选择K除对象之外的所有内容(在引擎盖下T使用)。Exclude
Omit<T, K> selects everything but K from object T (using Exclude under the hood).
我们已经了解了什么Partial<T>是。
And we already learned what Partial<T> does.
根据您喜欢的读取类型的方式,这种辅助类型的组合可以更易于阅读和理解,特别是因为内置类型在开发人员中更为人所知。
Depending on how you like to read types, this combination of helper types can be easier to read and understand, especially since the built-in types are much better known among developers.
只有一个问题:如果你将鼠标悬停在新生成的类型上,TypeScript 会显示该类型的生成方式,而不是实际的属性。使用8.3 节Remap中的辅助类型,我们可以使类型更具可读性和可用性:
There is only one problem: if you hover over your newly generated types, TypeScript will show you how the type is made, not what the actual properties are. With the Remap helper type from Recipe 8.3, we can make our types more readable and usable:
typeSetOptional<T,KextendskeyofT>=Remap<Partial<Pick<T,K>>&Omit<T,K>>;
typeSetOptional<T,KextendskeyofT>=Remap<Partial<Pick<T,K>>&Omit<T,K>>;
如果您将类型参数视为函数接口,那么您可能还需要考虑类型参数。您可以做的一项优化是将第二个参数(选定的对象键)设置为默认值:
If you think about your type arguments as a function interface, you might want to think about your type parameters as well. One optimization you could do is to set the second argument—the selected object keys—to a default value:
typeSetOptional<T,KextendskeyofT=keyofT>=Remap<Partial<Pick<T,K>>&Omit<T,K>>;
typeSetOptional<T,KextendskeyofT=keyofT>=Remap<Partial<Pick<T,K>>&Omit<T,K>>;
使用K extends keyof T = keyof T,我们可以确保将所有属性键设置为可选,并且仅在需要时才选择特定键。我们的辅助类型变得更加灵活。
With K extends keyof T = keyof T, we can make sure that we set all property keys as optional, and only select specific ones if we need them. Our helper type just became a little bit more flexible.
同样,您可以开始为其他情况创建类型,例如SetRequired,您想要确保某些键是绝对需要的:
In the same vein, you can start creating types for other situations, like SetRequired, where you want to make sure that some keys are definitely required:
typeSetRequired<T,KextendskeyofT=keyofT>=Remap<Required<Pick<T,K>>&Omit<T,K>>;
typeSetRequired<T,KextendskeyofT=keyofT>=Remap<Required<Pick<T,K>>&Omit<T,K>>;
或者OnlyRequired,您提供的所有键都是必需的,但其余的键是可选的:
Or OnlyRequired, where all keys you provide are required, but the rest are optional:
typeOnlyRequired<T,KextendskeyofT=keyofT>=Remap<Required<Pick<T,K>>&Partial<Omit<T,K>>>;
typeOnlyRequired<T,KextendskeyofT=keyofT>=Remap<Required<Pick<T,K>>&Partial<Omit<T,K>>>;
最好的事情是:您最终会获得可以在多个项目中使用的多种辅助类型。
The best thing: you end up with an arsenal of helper types that can be used throughout multiple projects.
创建对嵌套对象执行相同操作的递归辅助类型。
Create recursive helper types that do the same operation on nested objects.
假设您的应用具有可供用户配置的不同设置。为了方便您随时间扩展设置,您仅存储一组默认设置与用户配置的设置之间的差异:
Say that your application has different settings that can be configured by users. To make it easy for you to extend settings over time, you store only the difference between a set of defaults and the settings your user configured:
typeSettings={mode:"light"|"dark";playbackSpeed:number;subtitles:{active:boolean;color:string;};};constdefaults:Settings={mode:"dark",playbackSpeed:1.0,subtitles:{active:false,color:"white",},};
typeSettings={mode:"light"|"dark";playbackSpeed:number;subtitles:{active:boolean;color:string;};};constdefaults:Settings={mode:"dark",playbackSpeed:1.0,subtitles:{active:false,color:"white",},};
该函数applySettings采用默认值和用户的设置。您将它们定义为Partial<Settings>,因为用户只需提供一些键;其余的将从默认设置中获取:
The function applySettings takes both the defaults and the settings from your users. You defined them as Partial<Settings>, since the user needs to provide only some keys; the rest will be taken from the default settings:
functionapplySettings(defaultSettings:Settings,userSettings:Partial<Settings>):Settings{return{...defaultSettings,...userSettings};}
functionapplySettings(defaultSettings:Settings,userSettings:Partial<Settings>):Settings{return{...defaultSettings,...userSettings};}
如果您需要在第一级设置某些属性,这种方法非常有效:
This works really well if you need to set certain properties on the first level:
letsettings=applySettings(defaults,{mode:"light"});
letsettings=applySettings(defaults,{mode:"light"});
但是如果你想要修改对象深处的特定属性,这会导致问题,例如设置subtitles为active:
But this causes problems if you want to modify specific properties deeper down in your object, like setting subtitles to active:
letsettings=applySettings(defaults,{subtitles:{active:true}});// ^// Property 'color' is missing in type '{ active: true; }'// but required in type '{ active: boolean; color: string; }'.(2741)
letsettings=applySettings(defaults,{subtitles:{active:true}});// ^// Property 'color' is missing in type '{ active: true; }'// but required in type '{ active: boolean; color: string; }'.(2741)
TypeScript 抱怨说subtitles你需要提供整个对象。这是因为Partial<T>— 就像它的兄弟Required<T>和Readonly<T>一样 — 只修改对象的第一层。嵌套对象将被视为简单值。
TypeScript complains that for subtitles you need to provide the entire object. This is because Partial<T>—like its siblings Required<T> and Readonly<T>—modifies only the first level of an object. Nested objects will be treated as simple values.
为了改变这种情况,我们需要创建一个名为的新类型DeepPartial<T>,它以递归方式遍历每个属性并对每个级别应用可选的映射类型修饰符:
To change this, we need to create a new type called DeepPartial<T>, which recursively goes through every property and applies the optional mapped type modifier for each level:
typeDeepPartial<T>={[KinkeyofT]?:DeepPartial<T[K]>;};
typeDeepPartial<T>={[KinkeyofT]?:DeepPartial<T[K]>;};
由于 TypeScript 在原始值处停止了递归,因此第一稿已经运行良好,但它可能会导致不可读的输出。一个简单的条件检查我们是否仅在处理对象时进行深入处理,这使得我们的类型更加健壮,结果也更具可读性:
The first draft already works well, thanks to TypeScript stopping recursion at primitive values, but it has the potential to result in unreadable output. A simple condition that checks that we go deep only if we are dealing with an object makes our type much more robust and the result more readable:
typeDeepPartial<T>=Textendsobject?{[KinkeyofT]?:DeepPartial<T[K]>;}:T;
typeDeepPartial<T>=Textendsobject?{[KinkeyofT]?:DeepPartial<T[K]>;}:T;
例如,DeepPartial<Settings>产生以下输出:
For example, DeepPartial<Settings> results in the following output:
typeDeepPartialSettings={mode?:"light"|"dark"|undefined;playbackSpeed?:number|undefined;subtitles?:{active?:boolean|undefined;color?:string|undefined;}|undefined;};
typeDeepPartialSettings={mode?:"light"|"dark"|undefined;playbackSpeed?:number|undefined;subtitles?:{active?:boolean|undefined;color?:string|undefined;}|undefined;};
这正是我们想要的。如果我们使用DeepPartial<T>in applySettings,我们会看到 的实际用法是applySettings有效的,但 TypeScript 会向我们发出另一个错误:
This is exactly what we’ve been aiming for. If we use DeepPartial<T> in applySettings, we see that the actual usage of applySettings works, but TypeScript greets us with another error:
functionapplySettings(defaultSettings:Settings,userSettings:DeepPartial<Settings>):Settings{return{...defaultSettings,...userSettings};// ^// Type '{ mode: "light" | "dark"; playbackSpeed: number;// subtitles: { active?: boolean | undefined;// color?: string | undefined; }; }' is not assignable to type 'Settings'.}
functionapplySettings(defaultSettings:Settings,userSettings:DeepPartial<Settings>):Settings{return{...defaultSettings,...userSettings};// ^// Type '{ mode: "light" | "dark"; playbackSpeed: number;// subtitles: { active?: boolean | undefined;// color?: string | undefined; }; }' is not assignable to type 'Settings'.}
这里,TypeScript 抱怨它无法将两个对象合并为一个结果为 的对象Settings,因为某些DeepPartial集合元素可能无法分配给Settings。这是真的!使用解构的对象合并也只在第一级起作用,就像Partial<T>我们定义的那样。这意味着如果我们applySettings像以前一样调用,我们将得到与 完全不同的类型settings:
Here, TypeScript complains that it can’t merge the two objects into something that results in Settings, as some of the DeepPartial set elements might not be assignable to Settings. And this is true! Object merge using destructuring also works only on the first level, just like Partial<T> has defined for us. This means that if we called applySettings like before, we would get a totally different type than for settings:
letsettings=applySettings(defaults,{subtitles:{active:true}});// results inletsettings={mode:"dark",playbackSpeed:1,subtitles:{active:true}};
letsettings=applySettings(defaults,{subtitles:{active:true}});// results inletsettings={mode:"dark",playbackSpeed:1,subtitles:{active:true}};
color都消失了!在这种情况下,TypeScript 的类型一开始可能不直观:为什么对象修改类型只深入一层?因为 JavaScript 只深入一层!但最终,它们会指出你原本无法发现的错误。
color is all gone! This is one situation where TypeScript’s type might be unintuitive at first: why do object modification types go only one level deep? Because JavaScript goes only one level deep! But ultimately, they point out bugs you wouldn’t have caught otherwise.
为了避免这种情况,您需要递归地应用您的设置。这可能很难自己实现,因此我们求助于lodash及其merge函数来实现
此功能:
To circumvent this situation, you need to apply your settings recursively. This can be nasty to implement yourself, so we resort to lodash and its merge function for
this functionality:
import{merge}from"lodash";functionapplySettings(defaultSettings:Settings,userSettings:DeepPartial<Settings>):Settings{returnmerge(defaultSettings,userSettings)}
import{merge}from"lodash";functionapplySettings(defaultSettings:Settings,userSettings:DeepPartial<Settings>):Settings{returnmerge(defaultSettings,userSettings)}
merge定义其接口来产生两个对象的交集:
merge defines its interface to produce an intersection of two objects:
functionmerge<TObject,TSource>(object:TObject,source:TSource):TObject&TSource{// ...}
functionmerge<TObject,TSource>(object:TObject,source:TSource):TObject&TSource{// ...}
再次重申,这正是我们想要的。Settings和的交集DeepPartial<Settings>也会产生两者的交集,这也是由于类型的性质所致Settings。
Again, exactly what we are looking for. An intersection of Settings and DeepPartial<Settings> also produces an intersection of both, which is—due to the nature of the types—Settings again.
因此,我们最终得到了富有表现力的类型,这些类型可以准确地告诉我们期望的内容、输出的正确结果,以及我们武器库中的另一种辅助类型。您可以以类似的方式创建DeepReadonly和DeepRequired。
So we end up with expressive types that tell us exactly what to expect, correct results for the output, and another helper type for our arsenal. You can create DeepReadonly and DeepRequired similarly.
使用Remap<T>和DeepRemap<T>帮助类型来改进编辑器提示。
Use the Remap<T> and DeepRemap<T> helper types to improve editor hints.
当您使用 TypeScript 的类型系统构造新类型时,通过使用辅助类型、复杂条件类型甚至简单的交集,您最终可能会得到难以解读的编辑器提示。
When you use TypeScript’s type system to construct new types, by using helper types, complex conditional types, or even simple intersections, you might end up with editor hints that are hard to decipher.
我们来看看8.1 节OnlyRequired。该类型使用四个辅助类型和一个交集来构造一个新类型,其中作为第二个类型参数提供的所有键都设置为必需,而所有其他键都设置为可选:
Let’s look at OnlyRequired from Recipe 8.1. The type uses four helper types and one intersection to construct a new type in which all keys provided as the second type parameter are set to required, while all others are set to optional:
typeOnlyRequired<T,KextendskeyofT=keyofT>=Required<Pick<T,K>>&Partial<Omit<T,K>>;
typeOnlyRequired<T,KextendskeyofT=keyofT>=Required<Pick<T,K>>&Partial<Omit<T,K>>;
这种编写类型的方式可以让您很好地了解正在发生的事情。您可以根据辅助类型的组合方式来了解功能。但是,当您在模型上实际使用这些类型时,您可能希望了解的不仅仅是类型的实际构造:
This way of writing types gives you a good idea of what’s happening. You can read the functionality based on how helper types are composed with one another. However, when you are actually using the types on your models, you might want to know more than the actual construction of the type:
typePerson={name:string;age:number;profession:string;};typeNameRequired=OnlyRequired<Person,"name">;
typePerson={name:string;age:number;profession:string;};typeNameRequired=OnlyRequired<Person,"name">;
如果将鼠标悬停在 上NameRequired,您会看到 TypeScript 会根据您提供的参数向您提供有关如何构造类型的信息,但编辑器提示不会显示结果,最终类型是使用这些辅助类型构造的。您可以在图 8-1中看到编辑器的反馈。
If you hover over NameRequired, you see that TypeScript gives you information on how the type was constructed based on the parameters you provide, but the editor hint won’t show you the result, the final type being constructed with those helper types. You can see the editor’s feedback in Figure 8-1.
为了使最终结果看起来像实际类型并阐明所有属性,我们必须使用一种简单但有效的类型Remap:
To make the final result look like an actual type and to spell out all the properties, we have to use a simple yet effective type called Remap:
typeRemap<T>={[KinkeyofT]:T[K];};
typeRemap<T>={[KinkeyofT]:T[K];};
Remap<T>只是一种对象类型,它会遍历每个属性并将其映射到定义的值。无需修改,无需过滤,只需输出输入的内容。TypeScript 将打印出映射类型的每个属性,因此您看到的不是构造,而是实际的类型,如图8-2所示。
Remap<T> is just an object type that goes through every property and maps it to the value defined. No modifications, no filters, just putting out what’s being put in. TypeScript will print out every property of mapped types, so instead of seeing the construction, you see the actual type, as shown in Figure 8-2.
Remap<T>, 的呈现NameRequired变得更加
可读太棒了!这已成为 TypeScript 实用程序类型库中的必备品。有些人称它为Debug;其他人称它为Simplify。Remap只是同一工具和同一效果的另一个名称:了解结果会是什么样子。
Beautiful! This has become a staple in TypeScript utility type libraries. Some call it Debug; others call it Simplify. Remap is just another name for the same tool and the same effect: getting an idea of what your result will look like.
与其他映射类型一样Partial<T>,Readonly<T>和Required<T>也Remap<T>仅在第一级起作用。Settings包括Subtitles类型的嵌套类型将重新映射到相同的输出,编辑器反馈也将相同:
Like other mapped types Partial<T>, Readonly<T>, and Required<T>, Remap<T> also works on the first level only. A nested type like Settings that includes the Subtitles type will be remapped to the same output, and the editor feedback will be the same:
typeSubtitles={active:boolean;color:string;};typeSettings={mode:"light"|"dark";playbackSpeed:number;subtitles:Subtitles;};
typeSubtitles={active:boolean;color:string;};typeSettings={mode:"light"|"dark";playbackSpeed:number;subtitles:Subtitles;};
但同时,如方案 8.2所示,我们可以创建一个递归变体,重新映射所有嵌套对象类型:
But also, as shown in Recipe 8.2, we can create a recursive variation that remaps all nested object types:
typeDeepRemap<T>=Textendsobject?{[KinkeyofT]:DeepRemap<T[K]>;}:T;
typeDeepRemap<T>=Textendsobject?{[KinkeyofT]:DeepRemap<T[K]>;}:T;
申请DeepRemap<T>范围Settings还将扩大Subtitles:
Applying DeepRemap<T> to Settings will also expand Subtitles:
typeSettingsRemapped=DeepRemap<Settings>;// results intypeSettingsRemapped={mode:"light"|"dark";playbackSpeed:number;subtitles:{active:boolean;color:string;};};
typeSettingsRemapped=DeepRemap<Settings>;// results intypeSettingsRemapped={mode:"light"|"dark";playbackSpeed:number;subtitles:{active:boolean;color:string;};};
使用Remap主要取决于个人喜好。有时你想了解实现,有时嵌套类型的简洁视图比扩展版本更具可读性。但在其他情况下,你实际上关心结果本身。在这些情况下,拥有一个Remap<T>方便且可用的辅助类型
绝对有帮助。
Using Remap is mostly a matter of taste. Sometimes you want to know about the implementation, and sometimes the terse view of nested types is more readable than the expanded versions. But in other scenarios, you actually care about the result itself. In those cases, having a Remap<T> helper type handy and available is
definitely helpful.
创建一个映射的辅助类型GetRequired<T>,该类型根据针对其所需对应项的子类型检查来过滤键。
Create a mapped helper type GetRequired<T> that filters keys based on a subtype check against its required counterpart.
可选属性对类型兼容性有巨大影响。一个简单的类型修饰符,问号,可以大大拓宽原始类型。它们允许我们定义可能存在的字段,但只有在我们进行额外检查后才能使用它们。
Optional properties have a tremendous effect on type compatibility. A simple type modifier, the question mark, widens the original type significantly. They allow us to define fields that might be there, but they can be used only if we do additional checks.
这意味着我们可以使我们的函数和接口与完全缺乏某些属性的类型兼容:
This means we can make our functions and interfaces compatible with types that lack certain properties entirely:
typePerson={name:string;age?:number;};functionprintPerson(person:Person):void{// ...}typeStudent={name:string;semester:number;};conststudent:Student={name:"Stefan",semester:37,};printPerson(student);// all good!
typePerson={name:string;age?:number;};functionprintPerson(person:Person):void{// ...}typeStudent={name:string;semester:number;};conststudent:Student={name:"Stefan",semester:37,};printPerson(student);// all good!
我们看到age定义在 中,Person但 中根本没有定义Student。由于它是可选的,因此它不会阻止我们使用printPerson类型的对象Student。兼容值的集合更广泛,因为我们可以使用age完全删除类型的对象。
We see that age is defined in Person but not at all defined in Student. Since it’s optional, it doesn’t keep us from using printPerson with objects of type Student. The set of compatible values is wider, as we can use objects of types that drop age entirely.
TypeScript 通过附加undefined到可选属性来解决这个问题。这是“它可能存在”的最真实表现。
TypeScript solves that by attaching undefined to properties that are optional. This is the truest representation of “it might be there.”
如果我们想检查属性键是否是必需的,这一事实很重要。让我们从最基本的检查开始。我们有一个对象,想检查所有键是否都是必需的。我们使用辅助类型Required<T>,它将所有属性修改为必需的。最简单的检查是查看对象类型(例如)是否Name是其对应类型的子集Required<T>:
This fact is important if we want to check if property keys are required or not. Let’s start by doing the most basic check. We have an object and want to check if all keys are required. We use the helper type Required<T>, which modifies all properties to be required. The simplest check is to see if an object type—for example, Name—is a subset of its Required<T> counterpart:
typeName={name:string;};typeTest=NameextendsRequired<Name>?true:false;// type Test = true
typeName={name:string;};typeTest=NameextendsRequired<Name>?true:false;// type Test = true
这里,Test结果是true,因为如果我们将所有属性更改为required使用Required<T>,我们仍然会得到相同的类型。但是,如果我们引入一个可选属性,情况就会发生变化:
Here, Test results in true, because if we change all properties to required using Required<T>, we still get the same type. However, things change if we introduce an optional property:
typePerson={name:string;age?:number;};typeTest=PersonextendsRequired<Person>?true:false;// type Test = false
typePerson={name:string;age?:number;};typeTest=PersonextendsRequired<Person>?true:false;// type Test = false
这里,Test结果是,因为具有可选属性的false类型比需要设置的接受更广泛的值集。与此检查相反,如果我们交换和,我们可以看到较窄的类型实际上是的子集:PersonageRequired<Person>agePersonRequired<Person>Required<Person>Person
Here, Test results in false, because type Person with the optional property age accepts a much broader set of values than Required<Person>, where age needs to be set. Contrary to this check, if we swap Person and Required<Person>, we can see that the narrower type Required<Person> is in fact a subset of Person:
typeTest=Required<Person>extendsPerson?true:false;// type Test = true
typeTest=Required<Person>extendsPerson?true:false;// type Test = true
到目前为止,我们检查的是整个对象是否具有必需的键。但我们真正想要的是获取一个仅包含设置为必需的属性键的对象。这意味着我们需要对每个属性键进行此检查。需要对一组键进行相同的检查是映射类型的一个很好的指标。
What we’ve checked so far is if the entire object has the required keys. But what we actually want is to get an object that includes only property keys that are set to required. This means we need to do this check with each property key. The need to iterate the same check over a set of keys is a good indicator for a mapped type.
我们的下一步是创建一个映射类型,对每个属性执行子集检查,以查看结果值是否包括undefined:
Our next step is to create a mapped type that does the subset check for each property, to see if the resulting values include undefined:
typeRequiredPerson={[KinkeyofPerson]:Person[K]extendsRequired<Person[K]>?true:false;};/*type RequiredPerson = {name: true;age?: true | undefined;}*/
typeRequiredPerson={[KinkeyofPerson]:Person[K]extendsRequired<Person[K]>?true:false;};/*type RequiredPerson = {name: true;age?: true | undefined;}*/
这是一个很好的猜测,但给出的结果并不奏效。每个属性解析为true,这意味着子集仅检查不带的值类型
undefined。这是因为Required<T>适用于对象,而不适用于原始类型。让我们获得更可靠结果的方法是检查是否Person[K]包含任何可空值。NonNullable<T>删除undefined和null:
This is a good guess but gives us results that don’t work. Each property resolves to true, meaning that the subset checks only for the value types without
undefined. This is because Required<T> works on objects, not on primitive types. Something that gets us more robust results is checking if Person[K] includes any nullable values. NonNullable<T> removes undefined and null:
typeRequiredPerson={[KinkeyofPerson]:Person[K]extendsNonNullable<Person[K]>?true:false;};/*type RequiredPerson = {name: true;age?: false | undefined;}*/
typeRequiredPerson={[KinkeyofPerson]:Person[K]extendsNonNullable<Person[K]>?true:false;};/*type RequiredPerson = {name: true;age?: false | undefined;}*/
这样好多了,但仍然不是我们想要的。undefined又回来了,因为它是由属性修饰符添加的。此外,该属性仍在类型中,我们想摆脱它。
That’s better, but still not where we want it to be. undefined is back again, as it’s being added by the property modifier. Also, the property is still in the type, and we want to get rid of it.
我们需要做的是减少可能的键集。因此,我们在映射键时对每个属性进行条件检查,而不是检查值。我们检查是否Person[K]是的子集Required<Person>[K],并对更大的子集进行适当的检查。如果是,我们打印出键K;否则,我们使用以下方法删除该属性never(参见配方 5.2):
What we need to do is reduce the set of possible keys. So instead of checking for the values, we do a conditional check on each property while we are mapping out keys. We check if Person[K] is a subset of Required<Person>[K], doing a proper check against the bigger subset. If this is the case, we print out the key K; otherwise, we drop the property using never (see Recipe 5.2):
typeRequiredPerson={[KinkeyofPersonasPerson[K]extendsRequired<Person>[K]?K:never]:Person[K];};
typeRequiredPerson={[KinkeyofPersonasPerson[K]extendsRequired<Person>[K]?K:never]:Person[K];};
这给了我们想要的结果。现在我们替换Person一个泛型类型参数,我们的辅助类型GetRequired<T>就完成了:
This gives us the results we want. Now we substitute Person for a generic type parameter and our helper type GetRequired<T> is done:
typeGetRequired<T>={[KinkeyofTasT[K]extendsRequired<T>[K]?K:never]:T[K];};
typeGetRequired<T>={[KinkeyofTasT[K]extendsRequired<T>[K]?K:never]:T[K];};
从这里开始,我们可以派生出类似这样的变体GetOptional<T>。但是,检查某些东西是否是可选的并不像检查某些属性键是否是必需的那么容易,但我们可以使用GetRequired<T>和keyof运算符来获取所有必需的属性键:
From here on, we can derive variations like GetOptional<T>. However, checking if something is optional is not as easy as checking if some property keys are required, but we can use GetRequired<T> and a keyof operator to get all the required property keys:
typeRequiredKeys<T>=keyofGetRequired<T>;
typeRequiredKeys<T>=keyofGetRequired<T>;
之后,我们使用RequiredKeys<T>将它们从目标对象中省略:
After that, we use the RequiredKeys<T> to omit them from our target object:
typeGetOptional<T>=Omit<T,RequiredKeys<T>>;
typeGetOptional<T>=Omit<T,RequiredKeys<T>>;
Again, a combination of multiple helper types produces derived, self-maintaining types.
创建一个Split<T>辅助类型,将对象拆分为单属性对象的联合。
Create a Split<T> helper type that splits an object into a union of one-property objects.
您的应用程序将一组 URL(例如,视频格式)存储在一个对象中,其中每个键标识一种不同的格式:
Your application stores a set of URLs—for example, for video formats—in an object where each key identifies a different format:
typeVideoFormatURLs={format360p:URL;format480p:URL;format720p:URL;format1080p:URL;};
typeVideoFormatURLs={format360p:URL;format480p:URL;format720p:URL;format1080p:URL;};
您想要创建一个loadVideo可以加载任何视频格式 URL 的函数,但需要加载至少一个 URL。
You want to create a function loadVideo that can load any of those video format URLs but needs to load at least one URL.
如果loadVideo接受类型的参数VideoFormatURLs,则需要提供所有视频格式的URL:
If loadVideo accepts parameters of type VideoFormatURLs, you need to provide all video format URLs:
functionloadVideo(formats:VideoFormatURLs){// tbd}loadVideo({format360p:newURL("..."),format480p:newURL("..."),format720p:newURL("..."),format1080p:newURL("..."),});
functionloadVideo(formats:VideoFormatURLs){// tbd}loadVideo({format360p:newURL("..."),format480p:newURL("..."),format720p:newURL("..."),format1080p:newURL("..."),});
但有些视频可能不存在,因此所有可用类型的子集实际上就是您正在寻找的。Partial<VideoFormatURLs>为您提供:
But some videos might not exist, so a subset of all available types is actually what you’re looking for. Partial<VideoFormatURLs> gives you that:
functionloadVideo(formats:Partial<VideoFormatURLs>){// tbd}loadVideo({format480p:newURL("..."),format720p:newURL("..."),});
functionloadVideo(formats:Partial<VideoFormatURLs>){// tbd}loadVideo({format480p:newURL("..."),format720p:newURL("..."),});
但由于所有键都是可选的,因此您还可以允许空对象作为有效参数:
But since all keys are optional, you would also allow the empty object as a valid parameter:
loadVideo({});
loadVideo({});
这会导致未定义的行为。您需要至少一个 URL 才能加载该视频。
This results in undefined behavior. You want to have at least one URL so you can load that video.
我们必须找到一种类型来表达我们期望至少一种可用的视频格式:这种类型允许我们传递所有和部分格式,但也阻止我们传递任何格式。
We have to find a type expressing that we expect at least one of the available video formats: a type that allows us to pass all of them and some of them but also prevents us from passing none.
让我们从“只有一个”的情况开始。我们不需要找到一种类型,而是创建一个联合类型,它将所有只有一个属性集的情况结合起来:
Let’s start with the “only one” cases. Instead of finding one type, let’s create a union type that combines all situations where there’s only one property set:
typeAvailableVideoFormats=|{format360p:URL;}|{format480p:URL;}|{format720p:URL;}|{format1080p:URL;};
typeAvailableVideoFormats=|{format360p:URL;}|{format480p:URL;}|{format720p:URL;}|{format1080p:URL;};
这样我们就可以传入只设置了一个属性的对象。接下来,让我们添加设置了两个属性的情况:
This allows us to pass in objects that only have one property set. Next, let’s add the situations where we have two properties set:
typeAvailableVideoFormats=|{format360p:URL;}|{format480p:URL;}|{format720p:URL;}|{format1080p:URL;};
typeAvailableVideoFormats=|{format360p:URL;}|{format480p:URL;}|{format720p:URL;}|{format1080p:URL;};
等等!这是同一种类型!但这就是联合类型的工作方式。如果不加以区分(参见3.2 节),联合类型将允许位于原始集合所有交集处的值,如图8-3所示。
Wait! That’s the same type! But that’s the way union types work. If they aren’t discriminated (see Recipe 3.2), union types will allow for values that are located at all intersections of the original set, as shown in Figure 8-3.
AvailableVideoFormats每个联合成员定义一组可能的值。交集描述两种类型重叠的值。所有可能的组合都可以用这个联合来表达。
Each union member defines a set of possible values. The intersections describe the values where both types overlap. All possible combinations can be expressed with this union.
现在我们知道了类型,从原始类型派生出类型就太好了。我们想将对象类型拆分为类型的联合,其中每个成员都只包含一个属性。
So now that we know the type, it would be fantastic to derive it from the original type. We want to split an object type into a union of types where each member contains exactly one property.
获取相关联合类型的一种方法VideoFormatURLs是使用keyof运算符:
One way to get a union type related to VideoFormatURLs is to use the keyof operator:
typeAvailableVideoFormats=keyofVideoFormatURLs;
typeAvailableVideoFormats=keyofVideoFormatURLs;
这将产生"format360p" | "format480p" | "format720p" | "format1080p"一个键的并集。我们可以使用keyof运算符来索引访问原始类型:
This yields "format360p" | "format480p" | "format720p" | "format1080p", a union of the keys. We can use the keyof operator to index access the original type:
typeAvailableVideoFormats=VideoFormatURLs[keyofVideoFormatURLs];
typeAvailableVideoFormats=VideoFormatURLs[keyofVideoFormatURLs];
这样得到的URL只是一种类型,但实际上它是值类型的并集。现在我们只需要找到一种方法来获取代表实际对象类型并与每个属性键相关的正确值。
This yields URL, which is just one type, but in reality it is a union of the types of values. Now we only need to find a way to get proper values that represent an actual object type and are related to each property key.
再读一遍这句话:“与每个属性键相关”。这需要一个映射类型!我们可以映射所有属性,VideoFormatURLs以获取对象右侧的属性键:
Read this phrase again: “related to each property key.” This calls for a mapped type! We can map through all VideoFormatURLs to get the property key to the righthand side of the object:
typeAvailableVideoFormats={[KinkeyofVideoFormatURLs]:K;};/* yieldstype AvailableVideoFormats = {format360p: "format360p";format480p: "format480p";format720p: "format720p";format1080p: "format1080p";}; */
typeAvailableVideoFormats={[KinkeyofVideoFormatURLs]:K;};/* yieldstype AvailableVideoFormats = {format360p: "format360p";format480p: "format480p";format720p: "format720p";format1080p: "format1080p";}; */
这样,我们就可以通过索引访问映射类型并获取每个元素的值类型。但我们不仅将键设置为右侧,还创建另一个对象类型,该对象类型将此字符串作为属性键并将其映射到相应的值类型:
With that, we can index access the mapped type and get value types for each element. But we’re not only setting the key to the righthand side but also creating another object type that takes this string as a property key and maps it to the respective value type:
typeAvailableVideoFormats={[KinkeyofVideoFormatURLs]:{[PinK]:VideoFormatURLs[P]};};/* yieldstypeAvailableVideoFormats={format360p:{format360p:URL;};format480p:{format480p:URL;};format720p:{format720p:URL;};format1080p:{format1080p:URL;};};
typeAvailableVideoFormats={[KinkeyofVideoFormatURLs]:{[PinK]:VideoFormatURLs[P]};};/* yieldstypeAvailableVideoFormats={format360p:{format360p:URL;};format480p:{format480p:URL;};format720p:{format720p:URL;};format1080p:{format1080p:URL;};};
现在我们可以再次使用索引访问来将右侧的每个值类型 grep 到一个联合中:
Now we can use index access again to grep each value type from the righthand side into a union:
typeAvailableVideoFormats={[KinkeyofVideoFormatURLs]:{[PinK]:VideoFormatURLs[P]};}[keyofVideoFormatURLs];/* yieldstype AvailableVideoFormats =| {format360p: URL;}| {format480p: URL;}| {format720p: URL;}| {format1080p: URL;};*/
typeAvailableVideoFormats={[KinkeyofVideoFormatURLs]:{[PinK]:VideoFormatURLs[P]};}[keyofVideoFormatURLs];/* yieldstype AvailableVideoFormats =| {format360p: URL;}| {format480p: URL;}| {format720p: URL;}| {format1080p: URL;};*/
这就是我们一直在寻找的!下一步,我们采用具体类型并用泛型替换它们,从而得到Split<T>辅助类型:
And that’s what we’ve been looking for! As a next step, we take the concrete types and substitute them with generics, resulting in the Split<T> helper type:
typeSplit<T>={[KinkeyofT]:{[PinK]:T[P];};}[keyofT];
typeSplit<T>={[KinkeyofT]:{[PinK]:T[P];};}[keyofT];
我们武器库中的另一种辅助类型。使用它可以loadVideo为我们提供我们一直想要的行为:
Another helper type in our arsenal. Using it with loadVideo gives us exactly the behavior we have been aiming for:
functionloadVideo(formats:Split<VideoFormatURLs>){// tbd}loadVideo({});// ^// Argument of type '{}' is not assignable to parameter// of type 'Split<VideoFormatURLs>'loadVideo({format480p:newURL("..."),});// all good
functionloadVideo(formats:Split<VideoFormatURLs>){// tbd}loadVideo({});// ^// Argument of type '{}' is not assignable to parameter// of type 'Split<VideoFormatURLs>'loadVideo({format480p:newURL("..."),});// all good
Split<T>是一种很好的方式来了解基本类型系统功能如何显著地改变接口的行为,以及如何使用一些简单的类型技术(如映射类型、索引访问类型和属性键)来获得微小但功能强大的辅助类型。
Split<T> is a nice way to see how basic type system functionality can change the behavior of your interfaces significantly, and how some simple typing techniques like mapped types, index access types, and property keys can be used to get a tiny yet powerful helper type.
除了像方案 8.5中那样要求至少提供一个之外,您还需要提供用户只提供一个或全部或不提供一个的场景。
Next to requiring at least one like in Recipe 8.5, you also want to provide scenarios where users provide exactly one or all or none.
创建ExactlyOne<T>和AllOrNone<T, K>。两者都依赖于可选的 never技术与的导数的结合Split<T>。
Create ExactlyOne<T> and AllOrNone<T, K>. Both rely on the optional never technique in combination with a derivate of Split<T>.
Split<T>从配方 8.5开始,我们创建了一个不错的辅助类型,可以描述我们希望至少提供一个参数的场景。这是Partial<T>常规联合类型无法提供的。
With Split<T> from Recipe 8.5 we create a nice helper type that makes it possible to describe the scenario where we want at least one parameter provided. This is something that Partial<T> can’t provide for us, but regular union types can.
从这个想法开始,我们可能还会遇到这样的情况:我们希望用户只提供一个选项,确保他们不会添加太多选项。
Starting from this idea we, might also run into scenarios where we want our users to provide exactly one, making sure they don’t add too many options.
这里可以使用的一个技巧是可选的 never,我们在3.8 节中学到过它。除了要允许的所有属性之外,还要将所有不想允许的属性设置为可选,并将其值设置为never。这意味着,在编写属性名称时,TypeScript 希望您将其值设置为与 兼容的值never,但您无法做到这一点,因为never没有值。
One technique that can be used here is optional never, which we learned in Recipe 3.8. Next to all the properties you want to allow, you set all the properties you don’t want to allow to optional and their value to never. This means the moment you write the property name, TypeScript wants you to set its value to something that is compatible with never, which you can’t, as the never has no values.
联合类型的关键在于将所有属性名称放在一个排他或Split<T>关系中。我们得到一个联合类型,其中每个属性都已经具有:
A union type where we put all property names in an exclusive or relation is the key. We get a union type with each property already with Split<T>:
typeSplit<T>={[KinkeyofT]:{[PinK]:T[P];};}[keyofT];
typeSplit<T>={[KinkeyofT]:{[PinK]:T[P];};}[keyofT];
我们需要做的就是将每个元素与剩余的键相交,并将它们设置为可选的:
All we need to do is to intersect each element with the remaining keys and set them to optional never:
typeExactlyOne<T>={[KinkeyofT]:{[PinK]:T[P];}&{[PinExclude<keyofT,K>]?:never;// optional never};}[keyofT];
typeExactlyOne<T>={[KinkeyofT]:{[PinK]:T[P];}&{[PinExclude<keyofT,K>]?:never;// optional never};}[keyofT];
这样,生成的类型就更加广泛,但它准确地告诉我们要排除哪些属性:
With that, the resulting type is more extensive but tells us exactly which properties to exclude:
typeExactlyOneVideoFormat=({format360p:URL;}&{format480p?:never;format720p?:never;format1080p?:never;})|({format480p:URL;}&{format360p?:never;format720p?:never;format1080p?:never;})|({format720p:URL;}&{format320p?:never;format480p?:never;format1080p?:never;})|({format1080p:URL;}&{format320p?:never;format480p?:never;format720p?:never;});
typeExactlyOneVideoFormat=({format360p:URL;}&{format480p?:never;format720p?:never;format1080p?:never;})|({format480p:URL;}&{format360p?:never;format720p?:never;format1080p?:never;})|({format720p:URL;}&{format320p?:never;format480p?:never;format1080p?:never;})|({format1080p:URL;}&{format320p?:never;format480p?:never;format720p?:never;});
并且它按预期工作:
And it works as expected:
functionloadVideo(formats:ExactlyOne<VideoFormatURLs>){// tbd}loadVideo({format360p:newURL("..."),});// worksloadVideo({format360p:newURL("..."),format1080p:newURL("..."),});// ^// Argument of type '{ format360p: URL; format1080p: URL; }'// is not assignable to parameter of type 'ExactlyOne<VideoFormatURLs>'.
functionloadVideo(formats:ExactlyOne<VideoFormatURLs>){// tbd}loadVideo({format360p:newURL("..."),});// worksloadVideo({format360p:newURL("..."),format1080p:newURL("..."),});// ^// Argument of type '{ format360p: URL; format1080p: URL; }'// is not assignable to parameter of type 'ExactlyOne<VideoFormatURLs>'.
ExactlyOne<T>非常相似Split<T>,我们可以考虑扩展Split<T>功能以包含可选的 never 模式:
ExactlyOne<T> is so much like Split<T> that we could think of extending Split<T> with the functionality to include the optional never pattern:
typeSplit<T,OptionalNeverextendsboolean=false>={[KinkeyofT]:{[PinK]:T[P];}&(OptionalNeverextendsfalse?{}:{[PinExclude<keyofT,K>]?:never;});}[keyofT];typeExactlyOne<T>=Split<T,true>;
typeSplit<T,OptionalNeverextendsboolean=false>={[KinkeyofT]:{[PinK]:T[P];}&(OptionalNeverextendsfalse?{}:{[PinExclude<keyofT,K>]?:never;});}[keyofT];typeExactlyOne<T>=Split<T,true>;
我们添加了一个新的泛型类型参数OptionalNever,我们将其默认为false。然后,我们将创建新对象的部分与条件类型相交,该条件类型检查参数是否OptionalNever确实为 false。如果是,我们与空对象相交(保持原始对象不变);否则,我们将可选的 never 部分添加到对象。ExactlyOne<T>重构为Split<T, true>,我们激活OptionalNever标志。
We add a new generic type parameter OptionalNever, which we default to false. We then intersect the part where we create new objects with a conditional type that checks if the parameter OptionalNever is actually false. If so, we intersect with the empty object (leaving the original object intact); otherwise, we add the optional never part to the object. ExactlyOne<T> is refactored to Split<T, true>, where we activate the OptionalNever flag.
Split<T>另一种与or非常相似的场景ExactlyOne<T>是提供所有参数或不提供任何参数。考虑将视频格式分为标清(SD:360p 和 480p)和高清(HD:720p 和 1080p)。在您的应用中,您要确保如果用户提供 SD 格式,他们应该提供所有可能的格式。只有一种高清格式是可以的。
Another scenario very similar to Split<T> or ExactlyOne<T> is to provide all arguments or no arguments. Think of splitting video formats into standard definition (SD: 360p and 480p) and high definition (HD: 720p and 1080p). In your app, you want to make sure that if your users provide SD formats, they should provide all possible formats. It’s OK to have a single HD format.
这也是可选的 never 技术发挥作用的地方。我们定义一个类型,它需要所有选定的键,或者never如果只提供一个键,则将它们设置为:
This is also where the optional never technique comes in. We define a type that requires all selected keys or sets them to never if only one is provided:
typeAllOrNone<T,KeysextendskeyofT>=(|{[KinKeys]-?:T[K];// all available}|{[KinKeys]?:never;// or none});
typeAllOrNone<T,KeysextendskeyofT>=(|{[KinKeys]-?:T[K];// all available}|{[KinKeys]?:never;// or none});
如果您想确保提供所有高清格式,请通过交叉点将其余部分添加到其中:
If you want to make sure that you provide also all HD formats, add the rest to it via an intersection:
typeAllOrNone<T,KeysextendskeyofT>=(|{[KinKeys]-?:T[K];}|{[KinKeys]?:never;})&{[KinExclude<keyofT,Keys>]:T[K]// the rest, as it was defined}
typeAllOrNone<T,KeysextendskeyofT>=(|{[KinKeys]-?:T[K];}|{[KinKeys]?:never;})&{[KinExclude<keyofT,Keys>]:T[K]// the rest, as it was defined}
或者,如果高清格式完全是可选的,则通过以下方式添加它们Partial<T>:
Or if HD formats are totally optional, add them via a Partial<T>:
typeAllOrNone<T,KeysextendskeyofT>=(|{[KinKeys]-?:T[K];}|{[KinKeys]?:never;})&Partial<Omit<T,Keys>>;// the rest, but optional
typeAllOrNone<T,KeysextendskeyofT>=(|{[KinKeys]-?:T[K];}|{[KinKeys]?:never;})&Partial<Omit<T,Keys>>;// the rest, but optional
但是,你会遇到与方案 8.5中相同的问题,即你提供的值不包含任何格式。将全有或全无变体与以下方法相结合Split<T>,就是我们想要的解决方案:
But then you run into the same problem as in Recipe 8.5, where you can provide values that don’t include any formats at all. Intersecting the all or none variation with Split<T> is the solution we are aiming for:
typeAllOrNone<T,KeysextendskeyofT>=(|{[KinKeys]-?:T[K];}|{[KinKeys]?:never;})&Split<T>;
typeAllOrNone<T,KeysextendskeyofT>=(|{[KinKeys]-?:T[K];}|{[KinKeys]?:never;})&Split<T>;
并且它按预期工作:
And it works as intended:
functionloadVideo(formats:AllOrNone<VideoFormatURLs,"format360p"|"format480p">){// TBD}loadVideo({format360p:newURL("..."),format480p:newURL("..."),});// OKloadVideo({format360p:newURL("..."),format480p:newURL("..."),format1080p:newURL("..."),});// OKloadVideo({format1080p:newURL("..."),});// OKloadVideo({format360p:newURL("..."),format1080p:newURL("..."),});// ^ Argument of type '{ format360p: URL; format1080p: URL; }' is// not assignable to parameter of type// '({ format360p: URL; format480p: URL; } & ... (abbreviated)
functionloadVideo(formats:AllOrNone<VideoFormatURLs,"format360p"|"format480p">){// TBD}loadVideo({format360p:newURL("..."),format480p:newURL("..."),});// OKloadVideo({format360p:newURL("..."),format480p:newURL("..."),format1080p:newURL("..."),});// OKloadVideo({format1080p:newURL("..."),});// OKloadVideo({format360p:newURL("..."),format1080p:newURL("..."),});// ^ Argument of type '{ format360p: URL; format1080p: URL; }' is// not assignable to parameter of type// '({ format360p: URL; format480p: URL; } & ... (abbreviated)
如果我们仔细观察AllOrNone,我们可以轻松地用内置的辅助类型重写它:
If we look closely at what AllOrNone does, we can easily rewrite it with built-in helper types:
typeAllOrNone<T,KeysextendskeyofT>=(|Required<Pick<T,Keys>>|Partial<Record<Keys,never>>)&Split<T>;
typeAllOrNone<T,KeysextendskeyofT>=(|Required<Pick<T,Keys>>|Partial<Record<Keys,never>>)&Split<T>;
这无疑更易读,但也更符合类型系统中的元编程。您有一组辅助类型,可以将它们组合起来以创建新的辅助类型:几乎就像函数式编程语言,但在类型系统中是基于值集的。
This is arguably more readable but also more to the point of metaprogramming in the type system. You have a set of helper types, and you can combine them to create new helper types: almost like a functional programming language, but on sets of values, in the type system.
创建一个UnionToIntersection<T>使用逆变位置的辅助类型。
Create a UnionToIntersection<T> helper type that uses contravariant positions.
在8.5 节中,我们讨论了如何将模型类型拆分为其变体的联合。根据应用程序的工作方式,你可能希望从一开始就将模型定义为多个变体的联合类型:
In Recipe 8.5 we discussed how we can split a model type into a union of its variants. Depending on how your application works, you may want to define the model as a union type of several variants right from the beginning:
typeBasicVideoData={// tbd};typeFormat320={urls:{format320p:URL}};typeFormat480={urls:{format480p:URL}};typeFormat720={urls:{format720p:URL}};typeFormat1080={urls:{format1080p:URL}};typeVideo=BasicVideoData&(Format320|Format480|Format720|Format1080);
typeBasicVideoData={// tbd};typeFormat320={urls:{format320p:URL}};typeFormat480={urls:{format480p:URL}};typeFormat720={urls:{format720p:URL}};typeFormat1080={urls:{format1080p:URL}};typeVideo=BasicVideoData&(Format320|Format480|Format720|Format1080);
该类型Video允许您定义多种格式,但要求您至少定义
一种:
The type Video allows you to define several formats but requires you to define at
least one:
constvideo1:Video={// ...urls:{format320p:newURL("https://..."),},};// OKconstvideo2:Video={// ...urls:{format320p:newURL("https://..."),format480p:newURL("https://..."),},};// OKconstvideo3:Video={// ...urls:{format1080p:newURL("https://..."),},};// OK
constvideo1:Video={// ...urls:{format320p:newURL("https://..."),},};// OKconstvideo2:Video={// ...urls:{format320p:newURL("https://..."),format480p:newURL("https://..."),},};// OKconstvideo3:Video={// ...urls:{format1080p:newURL("https://..."),},};// OK
但是,将它们放在联合体中会产生一些副作用 - 例如,当您需要所有可用的键时:
However, putting them in a union has some side effects—for example, when you need all available keys:
typeFormatKeys=keyofVideo["urls"];// FormatKeys = never// This is not what we want here!functionselectFormat(format:FormatKeys):void{// tbd.}
typeFormatKeys=keyofVideo["urls"];// FormatKeys = never// This is not what we want here!functionselectFormat(format:FormatKeys):void{// tbd.}
您可能希望FormatKeys提供嵌套在 中的所有键的联合类型urls。但是,联合类型的索引访问会尝试找到最小公分母。在本例中,没有。要获取所有格式键的联合类型,您需要将所有键放在一个类型中:
You might expect FormatKeys to provide a union type of all keys that are nested in urls. Index access on a union type, however, tries to find the lowest common denominator. And in this case, there is none. To get a union type of all format keys, you need to have all keys within one type:
typeVideo=BasicVideoData&{urls:{format320p:URL;format480p:URL;format720p:URL;format1080p:URL;};};typeFormatKeys=keyofVideo["urls"];// type FormatKeys =// "format320p" | "format480p" | "format720p" | "format1080p";
typeVideo=BasicVideoData&{urls:{format320p:URL;format480p:URL;format720p:URL;format1080p:URL;};};typeFormatKeys=keyofVideo["urls"];// type FormatKeys =// "format320p" | "format480p" | "format720p" | "format1080p";
创建此类对象的一种方法是将联合类型修改为交集类型。
A way to create an object like this is to modify the union type to an intersection type.
在8.5 节中,用单一类型建模数据是可行的;在本节中,我们发现用联合类型建模数据更合我们心意。实际上,对于如何定义模型,并没有唯一的答案。使用最适合您的应用领域且不会过多妨碍您的表示形式。重要的是能够在需要时派生其他类型。这可以减少维护并允许您创建更健壮的类型。在第 12 章,尤其是12.1 节中,我们将讨论“低维护类型”的原则。
In Recipe 8.5, modeling data in a single type was the way to go; in this recipe, we see that modeling data as a union type is more to our liking. In reality, there is no single answer to how you define your models. Use the representation that best fits your application domain and that doesn’t get in your way too much. The important thing is to be able to derive other types as you need them. This reduces maintenance and allows you to create more robust types. In Chapter 12 and especially Recipe 12.1 we will look at the principle of “low maintenance types.”
在 TypeScript 中,将联合类型转换为交集类型是一项特殊的任务,需要对类型系统的内部工作原理有一定的了解。为了学习所有这些概念,我们先看看完成的类型,然后看看内部发生了什么:
Converting a union type to an intersection type is a peculiar task in TypeScript and requires some deep knowledge of the inner workings of the type system. To learn all these concepts, we look at the finished type, and then see what happens under the hood:
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)extends(x:inferR)=>any?R:never;
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)extends(x:inferR)=>any?R:never;
这里有很多内容需要解开:
There is a lot to unpack here:
我们有两种条件类型。第一种似乎总是导致分支true,那么我们为什么需要它呢?
We have two conditional types. The first one seems to always result in the true branch, so why do we need it?
第一个条件类型将类型包装在函数参数中,第二个条件类型再次将其解包。为什么这是必要的?
The first conditional type wraps the type in a function argument, and the second conditional type unwraps it again. Why is this necessary?
并且这两种条件类型如何将联合类型转换为交叉类型?
And how do both conditional types transform a union type to an intersection type?
我们来UnionToIntersection<T>一步步分析一下。
Let’s analyze UnionToIntersection<T> step by step.
在第一个条件中UnionToIntersection<T>,我们使用泛型类型参数作为裸类型:
In the first conditional within UnionToIntersection<T>, we use the generic type argument as a naked type:
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)//...
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)//...
这意味着我们检查是否T处于子类型条件,而不将其包装在其他类型中:
This means we check if T is in a subtype condition without wrapping it in some other type:
typeNaked<T>=Textends...;// a naked typetypeNotNaked<T>={o:T}extends...;// a non-naked type
typeNaked<T>=Textends...;// a naked typetypeNotNaked<T>={o:T}extends...;// a non-naked type
条件类型中的裸类型具有某些特性。如果T是联合,它们将为联合的每个组成部分运行条件类型。因此,对于裸类型,联合类型的条件变为条件类型的联合:
Naked types in conditional types have a certain feature. If T is a union, they run the conditional type for each constituent of the union. So with a naked type, a conditional of union types becomes a union of conditional types:
typeWrapNaked<T>=Textendsany?{o:T}:never;typeFoo=WrapNaked<string|number|boolean>;// A naked type, so this equals totypeFoo=WrapNaked<string>|WrapNaked<number>|WrapNaked<boolean>;// equals totypeFoo=stringextendsany?{o:string}:never|numberextendsany?{o:number}:never|booleanextendsany?{o:boolean}:never;typeFoo={o:string}|{o:number}|{o:boolean};
typeWrapNaked<T>=Textendsany?{o:T}:never;typeFoo=WrapNaked<string|number|boolean>;// A naked type, so this equals totypeFoo=WrapNaked<string>|WrapNaked<number>|WrapNaked<boolean>;// equals totypeFoo=stringextendsany?{o:string}:never|numberextendsany?{o:number}:never|booleanextendsany?{o:boolean}:never;typeFoo={o:string}|{o:number}|{o:boolean};
与非裸版相比:
As compared to the non-naked version:
typeWrapNaked<T>={o:T}extendsany?{o:T}:never;typeFoo=WrapNaked<string|number|boolean>;// A non-naked type, so this equals totypeFoo={o:string|number|boolean}extendsany?{o:string|number|boolean}:never;typeFoo={o:string|number|boolean};
typeWrapNaked<T>={o:T}extendsany?{o:T}:never;typeFoo=WrapNaked<string|number|boolean>;// A non-naked type, so this equals totypeFoo={o:string|number|boolean}extendsany?{o:string|number|boolean}:never;typeFoo={o:string|number|boolean};
微妙,但对于复杂类型来说却有很大不同!
Subtle, but considerably different for complex types!
在我们的示例中,我们使用裸类型并询问它是否扩展any(它总是会扩展;any是允许所有扩展的顶级类型):
In our example, we use the naked type and ask if it extends any (which it always does; any is the allow-it-all top type):
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)//...
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)//...
由于这个条件始终为真,我们将泛型类型包装在一个函数中,其中T是函数参数的类型。但我们为什么要这样做呢?
Since this condition is always true, we wrap our generic type in a function, where T is the type of the function’s parameter. But why are we doing that?
这就引出了第二个条件:
This leads to the second condition:
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)extends(x:inferR)=>any?R:never
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)extends(x:inferR)=>any?R:never
由于第一个条件始终为真,这意味着我们将类型包装在函数类型中,因此另一个条件也始终为真。我们基本上是在检查我们刚刚创建的类型是否是其自身的子类型。但我们不是传递T,而是推断出一种新类型R,并返回推断出的类型。
As the first condition always yields true, meaning that we wrap our type in a function type, the other condition also always yields true. We are basically checking if the type we just created is a subtype of itself. But instead of passing through T, we infer a new type R, and return the inferred type.
我们所做的是T通过函数类型包装和解开类型。
What we do is wrap and unwrap type T via a function type.
通过函数参数执行此操作会将新的推断类型R置于逆变位置。
Doing this via function arguments brings the new inferred type R in a contravariant position.
那么逆变是什么意思呢?逆变的反义词是协变,这与你从正常子类型中可以预料到的一样:
So what does contravariance mean? The opposite of contravariance is covariance, and what you would expect from normal subtyping:
declareletb:string;declareletc:string|number;c=b// OK
declareletb:string;declareletc:string|number;c=b// OK
string是 的子类型string | number; 的所有元素都string出现在 中string | number,因此我们可以将其分配b给c。c仍然按照我们最初的意图运行。 这就是协变。
string is a subtype of string | number; all elements of string appear in string | number, so we can assign b to c. c still behaves as we originally intended. This is covariance.
另一方面,这是行不通的:
This, on the other hand, won’t work:
typeFun<X>=(...args:X[])=>void;declareletf:Fun<string>;declareletg:Fun<string|number>;g=f// this cannot be assigned
typeFun<X>=(...args:X[])=>void;declareletf:Fun<string>;declareletg:Fun<string|number>;g=f// this cannot be assigned
我们不能将 赋值f给g,因为这样我们也能用f数字来调用 !我们错过了 的部分契约g。这就是逆变。
We can’t assign f to g, because then we would also be able to call f with a number! We miss part of the contract of g. This is contravariance.
有趣的是,逆变实际上就像交集一样工作:如果f接受string并且g接受string | number,那么 两者都接受的类型是(string | number) & string,即string。
The interesting thing is that contravariance effectively works like an intersection: if f accepts string and g accepts string | number, the type that is accepted by both is (string | number) & string, which is string.
当我们将类型置于条件类型中的逆变位置时,TypeScript 会从中创建一个交集。这意味着,由于我们从函数参数推断,TypeScript 知道我们必须履行完整的契约,从而在联合中创建所有成分的交集。
When we put types in contravariant positions within a conditional type, TypeScript creates an intersection out of it. Meaning that since we infer from a function argument, TypeScript knows that we have to fulfill the complete contract, creating an intersection of all constituents in the union.
基本上就是并集到交集。
Basically, union to intersection.
让我们来运行一下:
Let’s run it through:
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)extends(x:inferR)=>any?R:never;typeIntersected=UnionToIntersection<Video["urls"]>;// equals totypeIntersected=UnionToIntersection<{format320p:URL}|{format480p:URL}|{format720p:URL}|{format1080p:URL}>;
typeUnionToIntersection<T>=(Textendsany?(x:T)=>any:never)extends(x:inferR)=>any?R:never;typeIntersected=UnionToIntersection<Video["urls"]>;// equals totypeIntersected=UnionToIntersection<{format320p:URL}|{format480p:URL}|{format720p:URL}|{format1080p:URL}>;
我们有一个裸类型;这意味着我们可以进行条件并集:
We have a naked type; this means we can do a union of conditionals:
typeIntersected=|UnionToIntersection<{format320p:URL}>|UnionToIntersection<{format480p:URL}>|UnionToIntersection<{format720p:URL}>|UnionToIntersection<{format1080p:URL}>;
typeIntersected=|UnionToIntersection<{format320p:URL}>|UnionToIntersection<{format480p:URL}>|UnionToIntersection<{format720p:URL}>|UnionToIntersection<{format1080p:URL}>;
让我们扩展一下UnionToIntersection<T>:
Let’s expand UnionToIntersection<T>:
typeIntersected=|({format320p:URL}extendsany?(x:{format320p:URL})=>any:never)extends(x:inferR)=>any?R:never|({format480p:URL}extendsany?(x:{format480p:URL})=>any:never)extends(x:inferR)=>any?R:never|({format720p:URL}extendsany?(x:{format720p:URL})=>any:never)extends(x:inferR)=>any?R:never|({format1080p:URL}extendsany?(x:{format1080p:URL})=>any:never)extends(x:inferR)=>any?R:never;
typeIntersected=|({format320p:URL}extendsany?(x:{format320p:URL})=>any:never)extends(x:inferR)=>any?R:never|({format480p:URL}extendsany?(x:{format480p:URL})=>any:never)extends(x:inferR)=>any?R:never|({format720p:URL}extendsany?(x:{format720p:URL})=>any:never)extends(x:inferR)=>any?R:never|({format1080p:URL}extendsany?(x:{format1080p:URL})=>any:never)extends(x:inferR)=>any?R:never;
并评估第一个条件:
And evaluate the first conditional:
typeIntersected=|((x:{format320p:URL})=>any)extends(x:inferR)=>any?R:never|((x:{format480p:URL})=>any)extends(x:inferR)=>any?R:never|((x:{format720p:URL})=>any)extends(x:inferR)=>any?R:never|((x:{format1080p:URL})=>any)extends(x:inferR)=>any?R:never;
typeIntersected=|((x:{format320p:URL})=>any)extends(x:inferR)=>any?R:never|((x:{format480p:URL})=>any)extends(x:inferR)=>any?R:never|((x:{format720p:URL})=>any)extends(x:inferR)=>any?R:never|((x:{format1080p:URL})=>any)extends(x:inferR)=>any?R:never;
让我们评估第二个条件,我们推断R:
Let’s evaluate conditional two, where we infer R:
typeIntersected=|{format320p:URL}|{format480p:URL}|{format720p:URL}|{format1080p:URL};
typeIntersected=|{format320p:URL}|{format480p:URL}|{format720p:URL}|{format1080p:URL};
但是等等!R是从逆变位置推断出来的。我们必须进行交集;否则,我们将失去类型兼容性:
But wait! R is inferred from a contravariant position. We have to make an intersection; otherwise, we lose type compatibility:
typeIntersected={format320p:URL}&{format480p:URL}&{format720p:URL}&{format1080p:URL};
typeIntersected={format320p:URL}&{format480p:URL}&{format720p:URL}&{format1080p:URL};
这就是我们一直在寻找的!因此,应用到我们最初的例子:
And that’s what we have been looking for! So, applied to our original example:
typeFormatKeys=keyofUnionToIntersection<Video["urls"]>;
typeFormatKeys=keyofUnionToIntersection<Video["urls"]>;
FormatKeys现在是"format320p" | "format480p" | "format720p" | "format1080p"#。每当我们向原始联合添加另一种格式时,FormatKeys类型都会自动更新。维护一次;随处使用。
FormatKeys is now "format320p" | "format480p" | "format720p" | "format1080p"#. Whenever we add another format to the original union, the FormatKeys type updates automatically. Maintain once; use everywhere.
很可能type-fest已经拥有您需要的一切。
Chances are type-fest already has everything you need.
本章的整个思路是向您介绍一些有用的辅助类型,它们不属于标准 Typescript,但已被证明在许多情况下具有高度灵活性:单一用途的通用辅助类型,可以组合和组合以根据现有模型派生类型。您只需编写一次模型,所有其他类型都会自动更新。这种通过从其他类型派生类型来获得低维护类型的想法是 TypeScript 独有的,受到大量创建复杂应用程序或库的开发人员的赞赏。
The whole idea of this chapter was to introduce you to a couple of useful helper types that are not part of standard Typescript but have proven to be highly flexible for many scenarios: single-purpose generic helper types that can be combined and composed to derive types based on your existing models. You write your models once, and all other types get updated automatically. This idea of having low maintenance types, by deriving types from others, is unique to TypeScript and appreciated by tons of developers who create complex applications or libraries.
您可能最终会大量使用辅助类型,因此您开始将它们组合到一个实用程序库中以方便访问,但现有库中可能已经包含您需要的一切。使用一组定义良好的辅助类型并不是什么新鲜事,而且有很多库可以为您提供本章中看到的所有内容。有时它们完全相同,但名称不同;有时它们的想法类似,但解决方法不同。所有类型库很可能都涵盖了基础知识,但有一个库,type-fest,不仅有用,而且维护良好,文档齐全,使用广泛。
You might end up using your helper types a lot, so you start out combining them in a utility library for easy access, but chances are one of the existing libraries already has everything you need. Using a well-defined set of helper types is nothing new, and plenty out there give you everything you’ve seen in this chapter. Sometimes it’s exactly the same but under a different name; other times it’s a similar idea but solved differently. The basics are most likely covered by all type libraries, but one library, type-fest, is not only useful but actively maintained, well documented, and widely used.
Type-fest有几个方面使其脱颖而出。首先,它有详尽的文档。它的文档不仅包括某种辅助类型的用法Integer<T>,还包括用例和场景,告诉您可能想要在何处使用此辅助类型。一个例子是,它确保您提供的数字没有任何小数。
Type-fest has a few aspects that make it stand out. First, it’s extensively documented. Not only does its documentation include the usage of a certain helper type, but it also includes use cases and scenarios that tell you where you might want to use this helper type. One example is Integer<T>, which makes sure that the number you provide does not have any decimals.
这是一种几乎可以纳入TypeScript Cookbook 的实用程序类型,但我发现type-fest中的代码片段告诉了您需要了解的有关 该类型的所有信息:
This is a utility type that almost made it into TypeScript Cookbook, but I saw that giving you the snippet from type-fest tells you everything you need to know about the type:
/**A `number` that is an integer.You can't pass a `bigint` as they are already guaranteed to be integers.Use-case: Validating and documenting parameters.@example```import type {Integer} from 'type-fest';declare function setYear<T extends number>(length: Integer<T>): void;```@see NegativeInteger@see NonNegativeInteger@category Numeric*/// `${bigint}` is a type that matches a valid bigint// literal without the `n` (ex. 1, 0b1, 0o1, 0x1)// Because T is a number and not a string we can effectively use// this to filter out any numbers containing decimal pointsexporttypeInteger<Textendsnumber>=`${T}`extends`${bigint}`?T:never;
/**A `number` that is an integer.You can't pass a `bigint` as they are already guaranteed to be integers.Use-case: Validating and documenting parameters.@example```import type {Integer} from 'type-fest';declare function setYear<T extends number>(length: Integer<T>): void;```@see NegativeInteger@see NonNegativeInteger@category Numeric*/// `${bigint}` is a type that matches a valid bigint// literal without the `n` (ex. 1, 0b1, 0o1, 0x1)// Because T is a number and not a string we can effectively use// this to filter out any numbers containing decimal pointsexporttypeInteger<Textendsnumber>=`${T}`extends`${bigint}`?T:never;
该文件的其余部分处理负整数、非负整数、浮点数等。如果你想了解更多关于类型构造的信息,这是一个真正的信息宝库。
The rest of the file deals with negative integers, non-negative integers, floating point numbers, and so on. It’s a real treasure trove of information if you want to know more about how types are constructed.
其次,type-fest处理边缘情况。在8.2 节中,我们学习了递归类型并定义了DeepPartial<T>。它的type-fest对应部分PartialDeep<T>更为广泛:
Second, type-fest deals with edge cases. In Recipe 8.2, we learned about recursive types and defined DeepPartial<T>. Its type-fest counterpart, PartialDeep<T>, is a bit more extensive:
exporttypePartialDeep<T,OptsextendsPartialDeepOptions={}>=TextendsBuiltIns?T:TextendsMap<inferKeyType,inferValueType>?PartialMapDeep<KeyType,ValueType,Opts>:TextendsSet<inferItemType>?PartialSetDeep<ItemType,Opts>:TextendsReadonlyMap<inferKeyType,inferValueType>?PartialReadonlyMapDeep<KeyType,ValueType,Opts>:TextendsReadonlySet<inferItemType>?PartialReadonlySetDeep<ItemType,Opts>:Textends((...arguments:any[])=>unknown)?T|undefined:Textendsobject?TextendsReadonlyArray<inferItemType>?Opts['recurseIntoArrays']extendstrue?ItemType[]extendsT?readonlyItemType[]extendsT?ReadonlyArray<PartialDeep<ItemType|undefined,Opts>>:Array<PartialDeep<ItemType|undefined,Opts>>:PartialObjectDeep<T,Opts>:T:PartialObjectDeep<T,Opts>:unknown;/**Same as `PartialDeep`, but accepts only `Map`s and as inputs.Internal helper for `PartialDeep`.*/typePartialMapDeep<KeyType,ValueType,OptionsextendsPartialDeepOptions>={}&Map<PartialDeep<KeyType,Options>,PartialDeep<ValueType,Options>>;/**Same as `PartialDeep`, but accepts only `Set`s as inputs.Internal helper for `PartialDeep`.*/typePartialSetDeep<T,OptionsextendsPartialDeepOptions>={}&Set<PartialDeep<T,Options>>;/**Same as `PartialDeep`, but accepts only `ReadonlyMap`s as inputs.Internal helper for `PartialDeep`.*/typePartialReadonlyMapDeep<KeyType,ValueType,OptionsextendsPartialDeepOptions>={}&ReadonlyMap<PartialDeep<KeyType,Options>,PartialDeep<ValueType,Options>>;/**Same as `PartialDeep`, but accepts only `ReadonlySet`s as inputs.Internal helper for `PartialDeep`.*/typePartialReadonlySetDeep<T,OptionsextendsPartialDeepOptions>={}&ReadonlySet<PartialDeep<T,Options>>;/**Same as `PartialDeep`, but accepts only `object`s as inputs.Internal helper for `PartialDeep`.*/typePartialObjectDeep<ObjectTypeextendsobject,OptionsextendsPartialDeepOptions>={[KeyTypeinkeyofObjectType]?:PartialDeep<ObjectType[KeyType],Options>};
exporttypePartialDeep<T,OptsextendsPartialDeepOptions={}>=TextendsBuiltIns?T:TextendsMap<inferKeyType,inferValueType>?PartialMapDeep<KeyType,ValueType,Opts>:TextendsSet<inferItemType>?PartialSetDeep<ItemType,Opts>:TextendsReadonlyMap<inferKeyType,inferValueType>?PartialReadonlyMapDeep<KeyType,ValueType,Opts>:TextendsReadonlySet<inferItemType>?PartialReadonlySetDeep<ItemType,Opts>:Textends((...arguments:any[])=>unknown)?T|undefined:Textendsobject?TextendsReadonlyArray<inferItemType>?Opts['recurseIntoArrays']extendstrue?ItemType[]extendsT?readonlyItemType[]extendsT?ReadonlyArray<PartialDeep<ItemType|undefined,Opts>>:Array<PartialDeep<ItemType|undefined,Opts>>:PartialObjectDeep<T,Opts>:T:PartialObjectDeep<T,Opts>:unknown;/**Same as `PartialDeep`, but accepts only `Map`s and as inputs.Internal helper for `PartialDeep`.*/typePartialMapDeep<KeyType,ValueType,OptionsextendsPartialDeepOptions>={}&Map<PartialDeep<KeyType,Options>,PartialDeep<ValueType,Options>>;/**Same as `PartialDeep`, but accepts only `Set`s as inputs.Internal helper for `PartialDeep`.*/typePartialSetDeep<T,OptionsextendsPartialDeepOptions>={}&Set<PartialDeep<T,Options>>;/**Same as `PartialDeep`, but accepts only `ReadonlyMap`s as inputs.Internal helper for `PartialDeep`.*/typePartialReadonlyMapDeep<KeyType,ValueType,OptionsextendsPartialDeepOptions>={}&ReadonlyMap<PartialDeep<KeyType,Options>,PartialDeep<ValueType,Options>>;/**Same as `PartialDeep`, but accepts only `ReadonlySet`s as inputs.Internal helper for `PartialDeep`.*/typePartialReadonlySetDeep<T,OptionsextendsPartialDeepOptions>={}&ReadonlySet<PartialDeep<T,Options>>;/**Same as `PartialDeep`, but accepts only `object`s as inputs.Internal helper for `PartialDeep`.*/typePartialObjectDeep<ObjectTypeextendsobject,OptionsextendsPartialDeepOptions>={[KeyTypeinkeyofObjectType]?:PartialDeep<ObjectType[KeyType],Options>};
没有必要完成整个实现,但它应该能让你了解它们对某些实用程序类型的实现有多么强化。
There is no need to go through the entirety of this implementation, but it should give you an idea about how hardened their implementations for certain utility types are.
PartialDeep<T>非常全面,可以处理所有可能的边缘情况,但代价是过于复杂,难以被 TypeScript 类型检查器接受。根据您的使用情况,Recipe 8.2中的简单版本可能就是您所需要的。
PartialDeep<T> is extensive and deals with all possible edge cases, but it also comes at a cost of being complex and hard to swallow for the TypeScript type-checker. Depending on your use case, the simpler version from Recipe 8.2 might be the one you’re looking for.
第三,他们不会为了添加而添加辅助类型。他们的自述文件列出了被拒绝的类型以及被拒绝的原因:要么用例有限,要么存在更好的替代方案。就像所有事情一样,他们非常非常详细地记录了他们的选择。
Third, they don’t add helper types just for the sake of adding them. Their Readme file has a list of declined types and the reasoning behind the decline: either the use cases are limited or better alternatives exist. Just like everything, they document their choices really, really well.
第四,type-fest介绍了现有的辅助类型。辅助类型一直存在于 TypeScript 中,但过去几乎没有记录。几年前,我的博客试图成为内置辅助类型的资源,直到官方文档添加了关于实用程序类型的章节。实用程序类型不是仅通过使用 TypeScript 就能轻松掌握的东西。您需要了解它们的存在,并需要阅读它们。type -fest有一个专门介绍内置类型的部分,其中包含示例和用例。
Fourth, type-fest educates about existing helper types. Helper types have existed in TypeScript forever but barely have been documented in the past. Years ago, my blog attempted to be a resource on built-in helper types, until the official documentation added a chapter on utility types. Utility types are not something that you easily pick up just by using TypeScript. You need to understand that they exist and need to read up on them. type-fest has an entire section dedicated to built-ins, with examples and use cases.
最后,但并非最不重要的一点是,它被可靠的开源开发人员广泛采用和开发。它的创建者Sindre Sorhus几十年来一直致力于开源项目,并拥有出色的项目记录。type -fest只是另一个天才之举。您的很多工作很可能依赖于他的工作。
Last, but not least, it’s widely adopted and developed by reliable open source developers. Its creator, Sindre Sorhus, has worked on open source projects for decades and has a track record of fantastic projects. type-fest is just another stroke of genius. Chances are a lot of your work relies on his work.
使用type-fest,您可以获得另一种可添加到项目中的辅助类型资源。您可以自行决定是否要保留一小部分辅助类型,或者是否依赖社区的实现。
With type-fest you get another resource of helper types you can add to your project. Decide for yourself if you want to keep a small set of helper types or if you rely on the implementations by the community.
TypeScript 的首席架构师 Anders Hejlsberg 曾说过,他希望“TypeScript 成为 JavaScript 的瑞士”,这意味着它不会偏爱或致力于与单一框架兼容,而是试图迎合所有 JavaScript 框架和风格。过去,TypeScript 致力于实现装饰器,以说服 Google 不要为 Angular 开发 JavaScript 方言AtScript,即 TypeScript 加上装饰器。TypeScript 装饰器实现还可作为ECMAScript 关于装饰器的相应提案的模板。TypeScript 还理解 JSX 语法扩展,允许 React 或 Preact 等框架无限制地使用 TypeScript。
TypeScript’s lead architect, Anders Hejlsberg, once said that he envisions “TypeScript to be the Switzerland of JavaScript,” meaning that it doesn’t prefer or work toward compatibility with a single framework but rather tries to cater to all JavaScript frameworks and flavors. In the past, TypeScript worked on a decorator implementation to convince Google not to pursue the JavaScript dialect AtScript for Angular, which was TypeScript plus decorators. The TypeScript decorator implementation also serves as a template for a respective ECMAScript proposal on decorators. TypeScript also understands the JSX syntax extension, allowing frameworks like React or Preact to use TypeScript without limitations.
但即使 TypeScript 试图迎合所有 JavaScript 开发人员的需求,并付出巨大努力为大量框架集成新的有用功能,仍然有一些事情它做不到或不会做。可能是因为某个功能太小众,也可能是因为一个决定会对太多开发人员产生巨大影响。
But even if TypeScript tries to cater to all JavaScript developers and makes a huge effort to integrate new and useful features for a plethora of frameworks, there are still things it can’t or won’t do. Maybe because a certain feature is too niche, or maybe because a decision would have huge implications for too many developers.
这就是为什么 TypeScript 默认设计为可扩展的原因。TypeScript 的许多功能(如命名空间、模块和接口)都允许声明合并,这使您可以根据自己的喜好添加类型定义。
This is why TypeScript has been designed to be extensible by default. A lot of TypeScript’s features like namespaces, modules, and interfaces allow for declaration merging, which gives you the possibility to add type definitions to your liking.
在本章中,我们将了解 TypeScript 如何处理标准 JavaScript 功能,如模块、数组和对象。我们将了解它们的一些局限性,分析其局限性背后的原因,并提供合理的解决方法。您将看到 TypeScript 的设计非常灵活,可以适应各种 JavaScript 风格,从合理的默认值开始,并让您有机会在需要时进行扩展。
In this chapter, we look at how TypeScript deals with standard JavaScript functionality like modules, arrays, and objects. We will see some of their limitations, analyze the reasoning behind their limitations, and provide reasonable workarounds. You will see that TypeScript has been designed to be very flexible for various flavors of JavaScript, starting with sensible defaults, and giving you the opportunity to extend when you see fit.
使用for-in循环代替Object.keys并使用泛型类型参数锁定您的类型。
Use a for-in loop instead of Object.keys and lock your type using generic type parameters.
TypeScript 中一个突出的难题是尝试通过迭代对象的键来访问对象属性。这种模式在 JavaScript 中非常常见,但 TypeScript 似乎不惜一切代价阻止你使用它。我们使用这行简单的代码来迭代对象的属性:
A prominent head-scratcher in TypeScript is trying to access an object property via iterating through its keys. This pattern is so common in JavaScript, yet TypeScript seems to keep you from using it at all costs. We use this simple line to iterate over an object’s properties:
Object.keys(person).map(k=>person[k])
Object.keys(person).map(k=>person[k])
这会导致 TypeScript 向您抛出红色波浪线,开发人员翻看表格:“元素隐式具有类型,'any'因为类型的表达式'string'不能用于索引类型'Person'。”在这种情况下,经验丰富的 JavaScript 开发人员会觉得 TypeScript 在与他们作对。但与 TypeScript 中的所有决策一样,TypeScript 的行为有充分的理由。
It leads to TypeScript throwing red squigglies at you and developers flipping tables: “Element implicitly has an 'any' type because expression of type 'string' can’t be used to index type 'Person'.” In this situation, experienced JavaScript developers feel like TypeScript is working against them. But as with all decisions in TypeScript, there is a good reason why TypeScript behaves like this.
让我们找出原因。看一下这个函数:
Let’s find out why. Take a look at this function:
typePerson={name:string;age:number;};functionprintPerson(p:Person){Object.keys(p).forEach((k)=>{console.log(k,p[k]);// ^// Element implicitly has an 'any' type because expression// of type 'string' can't be used to index type 'Person'.});}
typePerson={name:string;age:number;};functionprintPerson(p:Person){Object.keys(p).forEach((k)=>{console.log(k,p[k]);// ^// Element implicitly has an 'any' type because expression// of type 'string' can't be used to index type 'Person'.});}
我们想要的只是打印一个Person通过 a 的键访问这些字段,从而打印出 a 的字段。TypeScript 不允许这样做。Object.keys(p)返回 a string[],它太宽,无法访问非常明确的对象形状Person。
All we want is to print a Person’s fields by accessing them through its keys. TypeScript won’t allow this. Object.keys(p) returns a string[], which is too wide to allow accessing a very defined object shape Person.
但为什么会这样呢?我们只访问可用的键,这不是很明显吗?这就是使用 的全部意义所在Object.keys!确实如此,但我们还可以传递 子类型的对象Person,这些对象可以具有比 中定义的更多属性Person:
But why is that? Isn’t it obvious that we only access keys that are available? That’s the whole point of using Object.keys! It is, but we are also able to pass objects that are subtypes of Person, which can have more properties than defined in Person:
constme={name:"Stefan",age:40,website:"https://fettblog.eu",};printPerson(me);// All good!
constme={name:"Stefan",age:40,website:"https://fettblog.eu",};printPerson(me);// All good!
printPerson仍应正常工作。它会打印更多属性,但不会中断。它仍然是的键p,因此每个属性都应该可以访问。但是如果您不只访问怎么办p?
printPerson still should work correctly. It prints more properties, but it doesn’t break. It’s still the keys of p, so every property should be accessible. But what if you don’t access only p?
假设Object.keys给你(keyof Person)[]。你可以轻松地写出这样的内容:
Let’s assume Object.keys gives you (keyof Person)[]. You can easily write something like this:
functionprintPerson(p:Person){constyou:Person={name:"Reader",age:NaN,};Object.keys(p).forEach((k)=>{console.log(k,you[k]);});}constme={name:"Stefan",age:40,website:"https://fettblog.eu",};printPerson(me);
functionprintPerson(p:Person){constyou:Person={name:"Reader",age:NaN,};Object.keys(p).forEach((k)=>{console.log(k,you[k]);});}constme={name:"Stefan",age:40,website:"https://fettblog.eu",};printPerson(me);
如果Object.keys(p)返回 类型的数组keyof Person[],则您也可以访问 的其他对象Person。这可能不合情理。在我们的示例中,我们只打印 undefined。但是如果您尝试使用这些值执行某些操作会怎样?这会在运行时中断。
If Object.keys(p) returns an array of type keyof Person[], you will be able to access other objects of Person, too. This might not add up. In our example, we just print undefined. But what if you try to do something with those values? This will break at runtime.
TypeScript 可以防止这种情况发生。虽然我们可能认为Object.keys是这样的keyof Person,但实际上,它可以做的远不止这些。
TypeScript prevents you from scenarios like this. While we might think Object.keys is keyof Person, in reality, it can be so much more.
缓解此问题的一种方法是使用类型保护:
One way to mitigate this problem is to use type guards:
functionisKey<T>(x:T,k:PropertyKey):kiskeyofT{returnkinx;}functionprintPerson(p:Person){Object.keys(p).forEach((k)=>{if(isKey(p,k))console.log(k,p[k]);// All fine!});}
functionisKey<T>(x:T,k:PropertyKey):kiskeyofT{returnkinx;}functionprintPerson(p:Person){Object.keys(p).forEach((k)=>{if(isKey(p,k))console.log(k,p[k]);// All fine!});}
但这增加了一个原本不应该存在的额外步骤。
But this adds an extra step that frankly shouldn’t be there.
还有另一种方法可以迭代对象,即使用for-in循环:
There’s another way to iterate over objects, using for-in loops:
functionprintPerson(p:Person){for(letkinp){console.log(k,p[k]);// ^// Element implicitly has an 'any' type because expression// of type 'string' can't be used to index type 'Person'.}}
functionprintPerson(p:Person){for(letkinp){console.log(k,p[k]);// ^// Element implicitly has an 'any' type because expression// of type 'string' can't be used to index type 'Person'.}}
TypeScript 会因为相同的原因抛出相同的错误,因为您仍然可以执行如下操作:
TypeScript will throw the same error for the same reason because you still can do things like this:
functionprintPerson(p:Person){constyou:Person={name:"Reader",age:NaN,};for(letkinp){console.log(k,you[k]);}}constme={name:"Stefan",age:40,website:"https://fettblog.eu",};printPerson(me);
functionprintPerson(p:Person){constyou:Person={name:"Reader",age:NaN,};for(letkinp){console.log(k,you[k]);}}constme={name:"Stefan",age:40,website:"https://fettblog.eu",};printPerson(me);
并且它会在运行时中断。但是,像这样编写会让您比Object.keys版本略胜一筹。如果您添加泛型,TypeScript 在这种情况下可以更加精确:
And it will break at runtime. However, writing it like this gives you a little edge over the Object.keys version. TypeScript can be much more exact in this scenario if you add a generic:
functionprintPerson<TextendsPerson>(p:T){for(letkinp){console.log(k,p[k]);// This works}}
functionprintPerson<TextendsPerson>(p:T){for(letkinp){console.log(k,p[k]);// This works}}
我们不再要求p是Person(从而与 的所有子类型兼容
Person),而是添加一个新的泛型类型参数T,它是 的子类型Person。这意味着与此函数签名兼容的所有类型仍然兼容,但在我们使用 的那一刻p,我们处理的是显式子类型,而不是更广泛的超类型Person。
Instead of requiring p to be Person (and thus be compatible with all subtypes of
Person), we add a new generic type parameter T that is a subtype of Person. This means all types that have been compatible with this function signature are still compatible, but the moment we use p, we are dealing with an explicit subtype, not the broader supertype Person.
我们T用兼容的东西代替Person来替代,但 TypeScript 知道它足够具体,可以防止出现错误。
We substitute T for something that is compatible with Person but where TypeScript knows that it’s concrete enough to prevent errors.
上述代码可以正常工作。k是 类型keyof T。 这就是为什么我们可以访问p,它是 类型T。 而且这种技术仍然阻止我们访问缺少特定属性的类型:
The preceding code works. k is of type keyof T. That’s why we can access p, which is of type T. And this technique still prevents us from accessing types that lack specific properties:
functionprintPerson<TextendsPerson>(p:T){constyou:Person={name:"Reader",age:NaN,};for(letkinp){console.log(k,you[k]);// ^// Type 'Extract<keyof T, string>' cannot be used to index type 'Person'}}
functionprintPerson<TextendsPerson>(p:T){constyou:Person={name:"Reader",age:NaN,};for(letkinp){console.log(k,you[k]);// ^// Type 'Extract<keyof T, string>' cannot be used to index type 'Person'}}
我们无法使用 访问 a Person。keyof T它们可能不同。但由于T是 的子类型Person,因此如果我们知道确切的属性名称,我们仍然可以分配属性:
We can’t access a Person with keyof T. They might be different. But since T is a subtype of Person, we still can assign properties, if we know the exact property names:
p.age=you.age
p.age=you.age
这正是我们想要的。
And that’s exactly what we want.
TypeScript 对其类型非常保守,乍一看可能有点奇怪,但它可以在你意想不到的情况下帮助你。我猜这是 JavaScript 开发人员通常会对编译器大喊大叫并认为他们在“对抗”它的地方,但也许 TypeScript 在你不知情的情况下拯救了你。对于这种情况令人讨厌的情况,TypeScript 至少为你提供了解决方法。
TypeScript being very conservative about its types here is something that might seem odd at first, but it helps you in scenarios you wouldn’t think of. I guess this is the part where JavaScript developers usually scream at the compiler and think they’re “fighting” it, but maybe TypeScript saved you without you knowing it. For situations where this gets annoying, TypeScript at least gives you ways to work around it.
在3.9 节中,我们讨论了如何有效地使用类型断言。类型断言是对类型系统的显式调用,表示某个类型应该是不同的类型,并且基于一些保护措施(例如,不说number实际上是string),TypeScript 会将此特定值视为新类型。
In Recipe 3.9 we spoke about how to effectively use type assertions. Type assertions are an explicit call to the type system to say that some type should be a different one, and based on some set of guardrails—for example, not saying number is actually string—TypeScript will treat this particular value as the new type.
借助 TypeScript 丰富而广泛的类型系统,有时类型断言是不可避免的。有时你甚至需要它们,如范例 3.9所示,我们使用fetchAPI 从后端获取 JSON 数据。一种方法是调用fetch并将结果分配给带注释的类型:
With TypeScript’s rich and extensive type system, sometimes type assertions are inevitable. Sometimes you even want them, as shown in Recipe 3.9 where we use the fetch API to get JSON data from a backend. One way is to call fetch and assign the results to an annotated type:
typePerson={name:string;age:number;};constppl:Person[]=awaitfetch("/api/people").then((res)=>res.json());
typePerson={name:string;age:number;};constppl:Person[]=awaitfetch("/api/people").then((res)=>res.json());
res.json()结果是any,1,并且所有内容都any可以通过类型注释更改为任何其他类型。不能保证结果实际上是
Person[]。
res.json() results in any,1 and everything that is any can be changed to any other type through a type annotation. There is no guarantee that the result is actually
Person[].
另一种方法是使用类型断言而不是类型注释:
The other way is to use a type assertion instead of a type annotation:
constppl=awaitfetch("/api/people").then((res)=>res.json())asPerson[];
constppl=awaitfetch("/api/people").then((res)=>res.json())asPerson[];
对于类型系统来说,这是同样的事情,但我们可以轻松扫描可能存在问题的情况。如果我们不根据类型验证传入的值(例如,使用 Zod;参见第 12.5 节),那么在这里使用类型断言是一种突出显示不安全操作的有效方法。
For the type system, this is the same thing, but we can easily scan situations where there might be problems. If we don’t validate our incoming values against types (with, for example, Zod; see Recipe 12.5), then having a type assertion here is an effective way of highlighting unsafe operations.
类型系统中的不安全操作是指我们告诉类型系统我们期望值属于某种类型,但类型系统本身却无法保证这确实会是真的。这种情况主要发生在应用程序的边界,我们会从某个地方加载数据、处理用户输入或使用内置方法解析数据。
Unsafe operations in a type system are situations where we tell the type system that we expect values to be of a certain type, but we don’t have any guarantee from the type system itself that this will actually be true. This happens mostly at the borders of our application, where we load data from someplace, deal with user input, or parse data with built-in methods.
可以使用某些关键字来突出显示不安全的操作,这些关键字指示类型系统中的显式更改。类型断言 ( as)、类型谓词 ( is) 或断言签名 ( asserts) 可帮助我们找到这些情况。在某些情况下,TypeScript 甚至会强迫我们遵守其类型视图或根据我们的情况明确更改规则。但并非总是如此。
Unsafe operations can be highlighted by using certain keywords that indicate an explicit change in the type system. Type assertions (as), type predicates (is), or assertion signatures (asserts) help us find those situations. In some cases, TypeScript even forces us either to comply with its view of types or to explicitly change the rules based on our situations. But not always.
当我们从某个后端获取数据时,注释就像编写类型断言一样容易。如果我们不强迫自己使用正确的技术,这样的事情可能会被忽略。
When we fetch data from some backend, it is just as easy to annotate as it is to write a type assertion. Things like that can be overlooked if we don’t force ourselves to use the correct technique.
但是我们可以帮助 TypeScript 帮助我们做正确的事情。问题在于对 的调用res.json(),它来自lib.dom.d.tsBody中的接口:
But we can help TypeScript help us do the right thing. The problem is the call to res.json(), which comes from the Body interface in lib.dom.d.ts:
interfaceBody{readonlybody:ReadableStream<Uint8Array>|null;readonlybodyUsed:boolean;arrayBuffer():Promise<ArrayBuffer>;blob():Promise<Blob>;formData():Promise<FormData>;json():Promise<any>;text():Promise<string>;}
interfaceBody{readonlybody:ReadableStream<Uint8Array>|null;readonlybodyUsed:boolean;arrayBuffer():Promise<ArrayBuffer>;blob():Promise<Blob>;formData():Promise<FormData>;json():Promise<any>;text():Promise<string>;}
调用json()返回一个Promise<any>,并且any是松散的类型,其中 TypeScript 完全忽略任何类型检查。我们需要any的谨慎兄弟unknown。得益于声明合并,我们可以覆盖Body类型定义并定义json()以使其更具限制性:
The json() call returns a Promise<any>, and any is the loosey-goosey type where TypeScript just ignores any type-check at all. We would need any’s cautious brother, unknown. Thanks to declaration merging, we can override the Body type definition and define json() to be a bit more restrictive:
interfaceBody{json():Promise<unknown>;}
interfaceBody{json():Promise<unknown>;}
当我们进行类型注释时,TypeScript 会向我们大喊,我们不能分配unknown给Person[]:
The moment we do a type annotation, TypeScript yells at us that we can’t assign unknown to Person[]:
constppl:Person[]=awaitfetch("/api/people").then((res)=>res.json());// ^// Type 'unknown' is not assignable to type 'Person[]'.ts(2322)
constppl:Person[]=awaitfetch("/api/people").then((res)=>res.json());// ^// Type 'unknown' is not assignable to type 'Person[]'.ts(2322)
但是如果我们进行类型断言,TypeScript 仍然会正常运行:
But TypeScript is still happy if we do a type assertion:
constppl=awaitfetch("/api/people").then((res)=>res.json())asPerson[];
constppl=awaitfetch("/api/people").then((res)=>res.json())asPerson[];
这样我们就可以强制 TypeScript 突出显示不安全操作. 2
And with that, we can force TypeScript to highlight unsafe operations.2
创建一个包装函数并使用断言签名来改变对象的类型。
Create a wrapper function and use assertion signatures to change the object’s type.
在 JavaScript 中,你可以使用 动态定义对象属性Object.defineProperty。如果你希望属性是只读的,这很有用。想象一下一个存储对象,它有一个最大值,并且不应该被覆盖:
In JavaScript, you can define object properties on the fly with Object.defineProperty. This is useful if you want your properties to be read-only. Think of a storage object that has a maximum value that shouldn’t be overwritten:
conststorage={currentValue:0};Object.defineProperty(storage,'maxValue',{value:9001,writable:false});console.log(storage.maxValue);// 9001storage.maxValue=2;console.log(storage.maxValue);// still 9001
conststorage={currentValue:0};Object.defineProperty(storage,'maxValue',{value:9001,writable:false});console.log(storage.maxValue);// 9001storage.maxValue=2;console.log(storage.maxValue);// still 9001
defineProperty和属性描述符非常复杂。它们允许您使用通常为内置对象保留的属性执行所有操作。因此它们在较大的代码库中很常见。TypeScript 存在以下问题defineProperty:
defineProperty and property descriptors are very complex. They allow you to do everything with properties that usually is reserved for built-in objects. So they’re common in larger codebases. TypeScript has a problem with defineProperty:
conststorage={currentValue:0};Object.defineProperty(storage,'maxValue',{value:9001,writable:false});console.log(storage.maxValue);// ^// Property 'maxValue' does not exist on type '{ currentValue: number; }'.
conststorage={currentValue:0};Object.defineProperty(storage,'maxValue',{value:9001,writable:false});console.log(storage.maxValue);// ^// Property 'maxValue' does not exist on type '{ currentValue: number; }'.
如果我们没有明确地断言新类型,我们就不会被maxValue绑定到 类型storage。但是,对于简单的用例,我们可以使用
断言签名来帮助自己。
If we don’t explicitly assert to a new type, we don’t get maxValue attached to the type of storage. However, for simple use cases, we can help ourselves using
assertion signatures.
虽然 TypeScript 在使用时可能不会出现对象更改Object.defineProperty,但团队将来可能会为此类情况添加类型或特殊行为。例如,使用in关键字检查对象是否具有某个属性多年来并没有影响类型。这种情况在 2022 年的TypeScript 4.9中发生了变化。
While TypeScript might not feature object changes when using Object.defineProperty, there is a chance that the team will add typings or special behavior for cases like this in the future. For example, checking if an object has a certain property using the in keyword didn’t affect types for years. This changed in 2022 with TypeScript 4.9.
想象一个assertIsNumber函数,你可以确保某个值的类型为number。否则,它会抛出一个错误。这类似于assertNode.js 中的函数:
Think of an assertIsNumber function where you can make sure some value is of type number. Otherwise, it throws an error. This is similar to the assert function in Node.js:
functionassertIsNumber(val:any){if(typeofval!=="number"){thrownewAssertionError("Not a number!");}}functionmultiply(x,y){assertIsNumber(x);assertIsNumber(y);// at this point I'm sure x and y are numbers// if one assert condition is not true, this position// is never reachedreturnx*y;}
functionassertIsNumber(val:any){if(typeofval!=="number"){thrownewAssertionError("Not a number!");}}functionmultiply(x,y){assertIsNumber(x);assertIsNumber(y);// at this point I'm sure x and y are numbers// if one assert condition is not true, this position// is never reachedreturnx*y;}
为了符合这样的行为,我们可以添加一个断言签名,告诉 TypeScript 我们在这个函数之后了解更多有关该类型的信息:
To comply with behavior like this, we can add an assertion signature that tells TypeScript that we know more about the type after this function:
functionassertIsNumber(val:any):assertsvalisnumberif(typeofval!=="number"){thrownewAssertionError("Not a number!");}}
functionassertIsNumber(val:any):assertsvalisnumberif(typeofval!=="number"){thrownewAssertionError("Not a number!");}}
这与类型谓词非常相似(参见范例 3.5if ),但没有像or 这样基于条件的结构的控制流switch:
This works a lot like type predicates (see Recipe 3.5) but without the control flow of a condition-based structure like if or switch:
functionmultiply(x,y){assertIsNumber(x);assertIsNumber(y);// Now also TypeScript knows that both x and y are numbersreturnx*y;}
functionmultiply(x,y){assertIsNumber(x);assertIsNumber(y);// Now also TypeScript knows that both x and y are numbersreturnx*y;}
如果仔细观察,您会发现这些断言签名可以动态更改参数或变量的类型。 这也是Object.defineProperty这样做的。
If you look at it closely, you can see those assertion signatures can change the type of a parameter or variable on the fly. This is what Object.defineProperty does as well.
以下帮助程序并非 100% 准确或完整。它可能有错误,并且可能无法解决defineProperty规范的每个极端情况。但它将为我们提供基本功能。首先,我们定义一个名为的新函数defineProperty,将其用作 的包装函数Object.defineProperty:
The following helper does not aim to be 100% accurate or complete. It might have errors, and it might not tackle every edge case of the defineProperty specification. But it will give us the basic functionality. First, we define a new function called defineProperty that we use as a wrapper function for Object.defineProperty:
functiondefineProperty<Objextendsobject,KeyextendsPropertyKey,PDescextendsPropertyDescriptor>(obj:Obj,prop:Key,val:PDesc){Object.defineProperty(obj,prop,val);}
functiondefineProperty<Objextendsobject,KeyextendsPropertyKey,PDescextendsPropertyDescriptor>(obj:Obj,prop:Key,val:PDesc){Object.defineProperty(obj,prop,val);}
我们使用三种泛型:
We work with three generics:
我们要修改的对象,类型为Obj,它是 的子类型object。
The object we want to modify, of type Obj, which is a subtype of object.
类型,它是(内置)Key的子类型: 。PropertyKeystring | number | symbol
Type Key, which is a subtype of PropertyKey (built-in): string | number | symbol.
PDescPropertyDescriptor, (内置)的子类型。这使我们能够定义具有其所有特性(可写性、可枚举性、可重构性)的属性。
PDesc, a subtype of PropertyDescriptor (built-in). This allows us to define the property with all its features (writability, enumerability, reconfigurability).
我们使用泛型是因为 TypeScript 可以将它们缩小到非常具体的单位类型。PropertyKey例如,是所有数字、字符串和符号。但是如果我们使用Key extends PropertyKey,我们可以精确地确定prop为,例如,类型"maxValue"。如果我们想通过添加更多属性来更改原始类型,这将很有帮助。
We use generics because TypeScript can narrow them to a very specific unit type. PropertyKey, for example, is all numbers, strings, and symbols. But if we use Key extends PropertyKey, we can pinpoint prop to be, for example, type "maxValue". This is helpful if we want to change the original type by adding more properties.
如果出现问题,该Object.defineProperty函数要么更改对象,要么抛出错误。这正是断言函数的作用。defineProperty因此,我们的自定义助手也会这样做。
The Object.defineProperty function either changes the object or throws an error should something go wrong. That’s exactly what an assertion function does. Our custom helper defineProperty thus does the same.
让我们添加一个断言签名。一旦defineProperty成功执行,我们的对象就会具有另一个属性。我们正在为此创建一些辅助类型。首先是签名:
Let’s add an assertion signature. Once defineProperty successfully executes, our object has another property. We are creating some helper types for that. The signature first:
functiondefineProperty<Objextendsobject,KeyextendsPropertyKey,PDescextendsPropertyDescriptor>(obj:Obj,prop:Key,val:PDesc):assertsobjisObj&DefineProperty<Key,PDesc>{Object.defineProperty(obj,prop,val);}
functiondefineProperty<Objextendsobject,KeyextendsPropertyKey,PDescextendsPropertyDescriptor>(obj:Obj,prop:Key,val:PDesc):assertsobjisObj&DefineProperty<Key,PDesc>{Object.defineProperty(obj,prop,val);}
obj然后是类型Obj(通过泛型缩小)和我们新定义的属性。
obj then is of type Obj (narrowed through a generic) and our newly defined property.
这是DefineProperty辅助类型:
This is the DefineProperty helper type:
typeDefineProperty<PropextendsPropertyKey,DescextendsPropertyDescriptor>=Descextends{writable:any,set(val:any):any}?never:Descextends{writable:any,get():any}?never:Descextends{writable:false}?Readonly<InferValue<Prop,Desc>>:Descextends{writable:true}?InferValue<Prop,Desc>:Readonly<InferValue<Prop,Desc>>;
typeDefineProperty<PropextendsPropertyKey,DescextendsPropertyDescriptor>=Descextends{writable:any,set(val:any):any}?never:Descextends{writable:any,get():any}?never:Descextends{writable:false}?Readonly<InferValue<Prop,Desc>>:Descextends{writable:true}?InferValue<Prop,Desc>:Readonly<InferValue<Prop,Desc>>;
首先,我们处理writable的属性PropertyDescriptor。它是一组条件,用于定义一些边缘情况以及原始属性描述符如何工作的条件:
First, we deal with the writable property of a PropertyDescriptor. It’s a set of conditions to define some edge cases and conditions of how the original property descriptors work:
如果我们设置writable任何属性访问器(get,set),我们就会失败。never告诉我们引发了一个错误。
If we set writable and any property accessor (get, set), we fail. never tells us that an error was thrown.
如果设置writable为false,则该属性为只读。我们遵从InferValue辅助类型。
If we set writable to false, the property is read-only. We defer to the InferValue helper type.
如果我们设置writable为true,则该属性不是只读的。我们也会推迟。
If we set writable to true, the property is not read-only. We defer as well.
最后一个默认情况与 相同writable: false,因此Readonly<InferValue<Prop, Desc>>. (Readonly<T>是内置的。)
The last default case is the same as writable: false, so Readonly<InferValue<Prop, Desc>>. (Readonly<T> is built-in.)
这是InferValue辅助类型,处理设置value属性:
This is the InferValue helper type, dealing with the set value property:
typeInferValue<PropextendsPropertyKey,Desc>=Descextends{get():any,value:any}?never:Descextends{value:inferT}?Record<Prop,T>:Descextends{get():inferT}?Record<Prop,T>:never;
typeInferValue<PropextendsPropertyKey,Desc>=Descextends{get():any,value:any}?never:Descextends{value:inferT}?Record<Prop,T>:Descextends{get():inferT}?Record<Prop,T>:never;
再次设置一组条件:
Again a set of conditions:
我们是否有 getter 和值集?Object.defineProperty会引发错误,所以never。
Do we have a getter and a value set? Object.defineProperty throws an error, so never.
如果我们设置了一个值,让我们推断这个值的类型,并使用我们定义的属性键和值类型创建一个对象。
If we have set a value, let’s infer the type of this value and create an object with our defined property key and the value type.
或者我们从 getter 的返回类型推断类型。
Or we infer the type from the return type of a getter.
我们忘记了其他任何事情。TypeScript 不允许我们在对象变成时使用它never。
Anything else we forget. TypeScript won’t let us work with the object as it’s becoming never.
有很多辅助类型,但大约需要 20 行代码才能正确完成:
Lots of helper types, but roughly 20 lines of code to get it right:
typeInferValue<PropextendsPropertyKey,Desc>=Descextends{get():any,value:any}?never:Descextends{value:inferT}?Record<Prop,T>:Descextends{get():inferT}?Record<Prop,T>:never;typeDefineProperty<PropextendsPropertyKey,DescextendsPropertyDescriptor>=Descextends{writable:any,set(val:any):any}?never:Descextends{writable:any,get():any}?never:Descextends{writable:false}?Readonly<InferValue<Prop,Desc>>:Descextends{writable:true}?InferValue<Prop,Desc>:Readonly<InferValue<Prop,Desc>>functiondefineProperty<Objextendsobject,KeyextendsPropertyKey,PDescextendsPropertyDescriptor>(obj:Obj,prop:Key,val:PDesc):assertsobjisObj&DefineProperty<Key,PDesc>{Object.defineProperty(obj,prop,val)}
typeInferValue<PropextendsPropertyKey,Desc>=Descextends{get():any,value:any}?never:Descextends{value:inferT}?Record<Prop,T>:Descextends{get():inferT}?Record<Prop,T>:never;typeDefineProperty<PropextendsPropertyKey,DescextendsPropertyDescriptor>=Descextends{writable:any,set(val:any):any}?never:Descextends{writable:any,get():any}?never:Descextends{writable:false}?Readonly<InferValue<Prop,Desc>>:Descextends{writable:true}?InferValue<Prop,Desc>:Readonly<InferValue<Prop,Desc>>functiondefineProperty<Objextendsobject,KeyextendsPropertyKey,PDescextendsPropertyDescriptor>(obj:Obj,prop:Key,val:PDesc):assertsobjisObj&DefineProperty<Key,PDesc>{Object.defineProperty(obj,prop,val)}
让我们看看 TypeScript 如何处理我们的更改:
Let’s see what TypeScript does with our changes:
conststorage={currentValue:0};defineProperty(storage,'maxValue',{writable:false,value:9001});storage.maxValue;// it's a numberstorage.maxValue=2;// Error! It's read-onlyconststorageName='My Storage';defineProperty(storage,'name',{get(){returnstorageName}});storage.name;// it's a string!// it's not possible to assign a value and a getterdefineProperty(storage,'broken',{get(){returnstorageName},value:4000});// storage is never because we have a malicious// property descriptorstorage;
conststorage={currentValue:0};defineProperty(storage,'maxValue',{writable:false,value:9001});storage.maxValue;// it's a numberstorage.maxValue=2;// Error! It's read-onlyconststorageName='My Storage';defineProperty(storage,'name',{get(){returnstorageName}});storage.name;// it's a string!// it's not possible to assign a value and a getterdefineProperty(storage,'broken',{get(){returnstorageName},value:4000});// storage is never because we have a malicious// property descriptorstorage;
虽然这可能没有涵盖所有内容,但已经为简单的属性定义做了很多工作 。
While this might not cover everything, there is already a lot done for simple property definitions.
创建具有类型谓词的通用辅助函数,您可以在其中更改 类型参数之间的关系。
Create generic helper functions with type predicates, where you change the relationship between type parameters.
我们创建一个名为 的数组actions,其中包含一组我们要执行的字符串格式的操作。此actions数组的结果类型为string[]。
We create an array called actions, which contains a set of actions in string format that we want to execute. The resulting type of this actions array is string[].
该execute函数接受任何字符串作为参数。我们检查这是否是有效操作,如果是,则执行某些操作:
The execute function takes any string as an argument. We check if this is a valid action, and if so, do something:
// actions: string[]constactions=["CREATE","READ","UPDATE","DELETE"];functionexecute(action:string){if(actions.includes(action)){// do something with action}}
// actions: string[]constactions=["CREATE","READ","UPDATE","DELETE"];functionexecute(action:string){if(actions.includes(action)){// do something with action}}
string[]如果我们想将 缩小到更具体的内容,即所有可能字符串的子集,那么事情会变得有点棘手。通过添加const context via as const,我们可以将 缩小actions到 类型readonly ["CREATE", "READ", "UPDATE", "DELETE"]。
It gets a little trickier if we want to narrow the string[] to something more concrete, a subset of all possible strings. By adding const context via as const, we can narrow actions to be of type readonly ["CREATE", "READ", "UPDATE", "DELETE"].
如果我们想要进行详尽性检查以确保所有可用操作都有案例,那么这很方便。然而,actions.includes不同意我们的观点:
This is handy if we want to do exhaustiveness checking to make sure we have cases for all available actions. However, actions.includes does not agree with us:
// Adding const context// actions: readonly ["CREATE", "READ", "UPDATE", "DELETE"]constactions=["CREATE","READ","UPDATE","DELETE"]asconst;functionexecute(action:string){if(actions.includes(action)){// ^// Argument of type 'string' is not assignable to parameter of type// '"CREATE" | "READ" | "UPDATE" | "DELETE"'.(2345)}}
// Adding const context// actions: readonly ["CREATE", "READ", "UPDATE", "DELETE"]constactions=["CREATE","READ","UPDATE","DELETE"]asconst;functionexecute(action:string){if(actions.includes(action)){// ^// Argument of type 'string' is not assignable to parameter of type// '"CREATE" | "READ" | "UPDATE" | "DELETE"'.(2345)}}
Array<T>为什么会这样?让我们看一下and的类型(由于const contextReadonlyArray<T>,我们使用后者):
Why is that? Let’s look at the typings of Array<T> and ReadonlyArray<T> (we work with the latter due to const context):
interfaceArray<T>{/*** Determines whether an array includes a certain element,* returning true or false as appropriate.* @param searchElement The element to search for.* @param fromIndex The position in this array at which* to begin searching for searchElement.*/includes(searchElement:T,fromIndex?:number):boolean;}interfaceReadonlyArray<T>{/*** Determines whether an array includes a certain element,* returning true or false as appropriate.* @param searchElement The element to search for.* @param fromIndex The position in this array at which* to begin searching for searchElement.*/includes(searchElement:T,fromIndex?:number):boolean;}
interfaceArray<T>{/*** Determines whether an array includes a certain element,* returning true or false as appropriate.* @param searchElement The element to search for.* @param fromIndex The position in this array at which* to begin searching for searchElement.*/includes(searchElement:T,fromIndex?:number):boolean;}interfaceReadonlyArray<T>{/*** Determines whether an array includes a certain element,* returning true or false as appropriate.* @param searchElement The element to search for.* @param fromIndex The position in this array at which* to begin searching for searchElement.*/includes(searchElement:T,fromIndex?:number):boolean;}
我们要搜索的元素 ( searchElement) 必须与数组本身属于同一类型!因此,如果我们有Array<string>(或string[]或ReadonlyArray<string>),我们只能搜索字符串。在我们的例子中,这意味着action需要是 类型"CREATE" | "READ" | "UPDATE" | "DELETE"。
The element we want to search for (searchElement) needs to be of the same type as the array itself! So if we have Array<string> (or string[] or ReadonlyArray<string>), we can search only for strings. In our case, this would mean that action needs to be of type "CREATE" | "READ" | "UPDATE" | "DELETE".
突然间,我们的程序变得不再有意义了。如果类型已经告诉我们它可以是四个字符串中的一个,为什么还要搜索它呢?如果我们将 的类型更改为action,"CREATE" | "READ" | "UPDATE" | "DELETE"就会actions.includes变得过时。如果我们不改变它,TypeScript 就会向我们抛出一个错误,这是理所当然的!
Suddenly, our program doesn’t make a lot of sense anymore. Why do we search for something if the type already tells us that it can be just one of four strings? If we change the type for action to "CREATE" | "READ" | "UPDATE" | "DELETE", actions.includes becomes obsolete. If we don’t change it, TypeScript throws an error at us, and rightfully so!
问题之一是 TypeScript 缺乏使用上限泛型等检查逆变类型的可能性。我们可以使用类似的构造来判断类型是否应该是类型的子集;我们无法检查类型是否是的超集。至少现在还不行!TextendsT
One of the problems is that TypeScript lacks the possibility to check for contravariant types with, for example, upper-bound generics. We can tell if a type should be a subset of type T with constructs like extends; we can’t check if a type is a superset of T. At least not yet!
那么我们能做什么呢?
So what can we do?
我想到的一个选择是改变includesinReadonlyArray的行为方式。得益于声明合并,我们可以添加自己的定义,ReadonlyArray这些定义在参数中更宽松,在结果中更具体,如下所示:
One option that comes to mind is changing how includes in ReadonlyArray should behave. Thanks to declaration merging, we can add our own definitions for ReadonlyArray that are a bit looser in the arguments and more specific in the result, like this:
interfaceReadonlyArray<T>{includes(searchElement:any,fromIndex?:number):searchElementisT;}
interfaceReadonlyArray<T>{includes(searchElement:any,fromIndex?:number):searchElementisT;}
这允许searchElement传递更广泛的值集(实际上是任何值!),如果条件为真,我们会通过类型谓词告诉 TypeScript (我们searchElement is T正在寻找的子集)。
This allows for a broader set of searchElement values to be passed (literally any!), and if the condition is true, we tell TypeScript through a type predicate that searchElement is T (the subset we are looking for).
事实证明,这种方法效果很好:
Turns out, this works pretty well:
constactions=["CREATE","READ","UPDATE","DELETE"]asconst;functionexecute(action:string){if(actions.includes(action)){// action: "CREATE" | "READ" | "UPDATE" | "DELETE"}}
constactions=["CREATE","READ","UPDATE","DELETE"]asconst;functionexecute(action:string){if(actions.includes(action)){// action: "CREATE" | "READ" | "UPDATE" | "DELETE"}}
但是,有一个问题。该解决方案有效,但需要假设什么是正确的以及什么需要检查。如果更改action为number,TypeScript 通常会抛出一个错误,提示您无法搜索该类型。actions只由组成string,那么为什么还要查看呢number?这是您想要捕获的错误:
There’s a problem, though. The solution works but takes the assumption of what’s correct and what needs to be checked. If you change action to number, TypeScript usually throws an error that you can’t search for that kind of type. actions only consists of string, so why even look at number? This is an error you want to catch:
// type number has no relation to actions at allfunctionexecute(action:number){if(actions.includes(action)){// do something}}
// type number has no relation to actions at allfunctionexecute(action:number){if(actions.includes(action)){// do something}}
随着我们对 的改变,我们失去了对的ReadonlyArray检查。虽然 的功能仍然按预期工作,但一旦我们在此过程中更改函数签名,我们可能就看不到正确的问题。searchElementanyaction.includes
With our change to ReadonlyArray, we lose this check as searchElement is any. While the functionality of action.includes still works as intended, we might not see the right problem once we change function signatures along the way.
此外,更重要的是,我们改变了内置类型的行为。这可能会改变其他地方的类型检查,并可能在长期内导致问题!
Also, and more important, we change the behavior of built-in types. This might change your type-checks somewhere else and might cause problems in the long run!
如果通过更改标准库的行为来进行类型修补,请确保在模块范围内进行,而不是全局进行。
If you do a type patch by changing behavior from the standard library, be sure to do this module scoped, and not globally.
还有另一种方法。
There is another way.
正如最初所述,问题之一是 TypeScript 缺乏检查值是否属于泛型参数超集的可能性。使用辅助函数,我们可以扭转这种关系:
As originally stated, one of the problems is that TypeScript lacks the possibility to check if a value belongs to a superset of a generic parameter. With a helper function, we can turn this relationship around:
functionincludes<TextendsU,U>(coll:ReadonlyArray<T>,el:U):elisT{returncoll.includes(elasT);}
functionincludes<TextendsU,U>(coll:ReadonlyArray<T>,el:U):elisT{returncoll.includes(elasT);}
该includes函数以ReadonlyArray<T>作为参数,并搜索 类型的元素U。我们通过泛型边界检查T extends U,这意味着U是 的超集T(或T的子集)U。如果该方法返回true,我们可以肯定地说el是较窄的类型U。
The includes function takes the ReadonlyArray<T> as an argument and searches for an element that is of type U. We check through our generic bounds that T extends U, which means that U is a superset of T (or T is a subset of U). If the method returns true, we can say for sure that el is of the narrower type U.
为了让实现工作,我们唯一需要做的就是在传递给时进行一些类型断言el。Array.prototype.includes原始问题仍然存在!el as T但是类型断言是可以的,因为我们已经在函数签名中检查了可能的问题。
The only thing that we need to make the implementation work is to do a little type assertion the moment we pass el to Array.prototype.includes. The original problem is still there! The type assertion el as T is OK, though, as we check possible problems already in the function signature.
这意味着当我们将其更改为时,action我们number会在整个代码中得到正确的错误:
This means the moment we change, for example, action to number, we get the right errors throughout our code:
functionexecute(action:number){if(includes(actions,action)){// ^// Argument of type 'readonly ["CREATE", "READ", "UPDATE", "DELETE"]'// is not assignable to parameter of type 'readonly number[]'.}}
functionexecute(action:number){if(includes(actions,action)){// ^// Argument of type 'readonly ["CREATE", "READ", "UPDATE", "DELETE"]'// is not assignable to parameter of type 'readonly number[]'.}}
这正是我们想要的行为。TypeScript 的一个巧妙之处在于,它希望我们更改数组,而不是我们正在寻找的元素。这是由于泛型类型参数之间的关系。
And this is the behavior we want. A nice touch is that TypeScript wants us to change the array, not the element we are looking for. This is due to the relationship between the generic type parameters.
如果您遇到类似的问题,相同的解决方案也有效Array.prototype.indexOf。
The same solutions also work if you run into similar troubles with Array.prototype.indexOf.
TypeScript 旨在使所有标准 JavaScript 功能正确无误,但有时您必须做出权衡。这种情况需要权衡:您是否允许比预期更宽松的参数列表,或者您是否会针对您应该了解更多信息的类型抛出错误?
TypeScript aims to get all standard JavaScript functionality correct, but sometimes you have to make trade-offs. This case calls for trade-offs: do you allow for an argument list that’s looser than you would expect, or do you throw errors for types where you already should know more?
类型断言、声明合并和其他工具可以帮助我们在类型系统无法帮助我们的情况下解决这个问题。直到它变得比以前更好,让我们在类型空间中走得更远。
Type assertions, declaration merging, and other tools help us get around that in situations where the type system can’t help us. Not until it becomes better than before, by allowing us to move even further in the type space.
filter使用声明合并来重载该方法Array。
Overload the filter method from Array using declaration merging.
有时你的集合可能包含空值(undefined或null):
Sometimes you have collections that could include nullish values (undefined or null):
// const array: (number | null | undefined)[]constarray=[1,2,3,undefined,4,null];
// const array: (number | null | undefined)[]constarray=[1,2,3,undefined,4,null];
为了继续工作,您需要从集合中删除这些空值。这通常使用filter的方法来完成Array,也许通过检查值的真实性null。和undefined是假的,所以它们被过滤掉:
To continue working, you want to remove those nullish values from your collection. This is typically done using the filter method of Array, maybe by checking the truthiness of a value. null and undefined are falsy, so they get filtered out:
constfiltered=array.filter((val)=>!!val);
constfiltered=array.filter((val)=>!!val);
检查值真实性的一个方便方法是将其传递给布尔构造函数。这很简短,切中要点,读起来非常优雅:
A convenient way of checking the truthiness of a value is by passing it to the Boolean constructor. This is short, on point, and very elegant to read:
// const array: (number | null | undefined)[]constfiltered=array.filter(Boolean);
// const array: (number | null | undefined)[]constfiltered=array.filter(Boolean);
但遗憾的是,它并没有改变我们的类型。我们仍然拥有null和undefined作为过滤数组的可能类型。
But sadly, it doesn’t change our type. We still have null and undefined as possible types for the filtered array.
通过开放Array接口并添加另一个声明filter,我们可以将这种特殊情况添加为重载:
By opening up the Array interface and adding another declaration for filter, we can add this special case as an overload:
interfaceArray<T>{filter(predicate:BooleanConstructor):NonNullable<T>[]}interfaceReadonlyArray<T>{filter(predicate:BooleanConstructor):NonNullable<T>[]}
interfaceArray<T>{filter(predicate:BooleanConstructor):NonNullable<T>[]}interfaceReadonlyArray<T>{filter(predicate:BooleanConstructor):NonNullable<T>[]}
这样,我们就可以摆脱空类型,并更加清楚地了解数组内容的类型:
And with that, we get rid of nullish types and have more clarity on the type of our array’s contents:
// const array: number[]constfiltered=array.filter(Boolean);
// const array: number[]constfiltered=array.filter(Boolean);
太棒了!有什么需要注意的吗?文字元组和数组。BooleanConstructor不仅可以过滤空值,还可以过滤假值。为了获得正确的元素,我们不仅必须返回NonNullable<T>,还必须引入一种检查真值的类型:
Neat! What’s the caveat? Literal tuples and arrays. BooleanConstructor filters not only nullish values but also falsy values. To get the right elements, we not only have to return NonNullable<T> but also introduce a type that checks for truthy values:
typeTruthy<T>=Textends""|false|0|0n?never:T;interfaceArray<T>{filter(predicate:BooleanConstructor):Truthy<NonNullable<T>>[];}interfaceReadonlyArray<T>{filter(predicate:BooleanConstructor):Truthy<NonNullable<T>>[];}// as const creates a readonly tupleconstarray=[0,1,2,3,``,-0,0n,false,undefined,null]asconst;// const filtered: (1 | 2 | 3)[]constfiltered=array.filter(Boolean);constnullOrOne:Array<0|1>=[0,1,0,1];// const onlyOnes: 1[]constonlyOnes=nullOrOne.filter(Boolean);
typeTruthy<T>=Textends""|false|0|0n?never:T;interfaceArray<T>{filter(predicate:BooleanConstructor):Truthy<NonNullable<T>>[];}interfaceReadonlyArray<T>{filter(predicate:BooleanConstructor):Truthy<NonNullable<T>>[];}// as const creates a readonly tupleconstarray=[0,1,2,3,``,-0,0n,false,undefined,null]asconst;// const filtered: (1 | 2 | 3)[]constfiltered=array.filter(Boolean);constnullOrOne:Array<0|1>=[0,1,0,1];// const onlyOnes: 1[]constonlyOnes=nullOrOne.filter(Boolean);
示例包括0n类型为 0 的元素BigInt。此类型仅从 ECMAScript 2020 开始可用。
The example includes 0n which is 0 in the BigInt type. This type is available only from ECMAScript 2020 on.
这给了我们正确的概念,告诉我们应该期望哪种类型,但是由于ReadonlyArray<T>采用的是元组元素的类型而不是元组本身的类型,我们丢失了元组内类型顺序的信息。
This gives us the right idea of which types to expect, but since ReadonlyArray<T> takes the tuple’s elements types and not the tuple type itself, we lose information on the order of types within the tuple.
与所有现有 TypeScript 类型的扩展一样,请注意这可能会引起副作用。将它们限定在本地范围并谨慎使用它们。
As with all extensions to existing TypeScript types, be aware that this might cause side effects. Scope them locally and use them carefully.
在模块和接口级别使用声明合并。
Use declaration merging on the module and interface level.
JSX是 JavaScript 的语法扩展,引入了类似 XML 的方式来描述和嵌套组件。基本上,所有可以描述为元素树的东西都可以用 JSX 来表达。流行的 React 框架的创建者引入了 JSX,以便在 JavaScript 中以类似 HTML 的方式编写和嵌套组件,它实际上被转换为一系列函数调用:
JSX is a syntax extension to JavaScript, introducing an XML-like way of describing and nesting components. Basically, everything that can be described as a tree of elements can be expressed in JSX. JSX was introduced by the creators of the popular React framework to make it possible to write and nest components in an HTML-like way within JavaScript, where it is actually transpiled to a series of function calls:
<buttononClick={()=>alert('YES')}>Clickme</button>// Transpiles to:React.createElement("button",{onClick:()=>alert('YES')},'Click me');
<buttononClick={()=>alert('YES')}>Clickme</button>// Transpiles to:React.createElement("button",{onClick:()=>alert('YES')},'Click me');
此后,即使与 React 的联系很少甚至没有联系,JSX 也已被许多框架采用。第 10 章中有更多关于 JSX 的内容。
JSX has since been adopted by many frameworks, even if there is little or no connection to React. There’s a lot more on JSX in Chapter 10.
TypeScript 的 React 类型化为所有可能的 HTML 元素提供了大量接口。但有时您的浏览器、框架或代码会比可能的情况稍微超前一些。
React typings for TypeScript come with lots of interfaces for all possible HTML elements. But sometimes your browsers, your frameworks, or your code are a little bit ahead of what’s possible.
假设您想使用 Chrome 中的最新图片功能并延迟加载图片。这是一项渐进式增强功能,因此只有了解发生了什么的浏览器才知道如何解释这一点。其他浏览器足够强大,不会在意:
Let’s say you want to use the latest image features in Chrome and load your images lazily. This is a progressive enhancement, so only browsers that understand what’s going on know how to interpret this. Other browsers are robust enough not to care:
<imgsrc="/awesome.jpg"loading="lazy"alt="What an awesome image"/>
<imgsrc="/awesome.jpg"loading="lazy"alt="What an awesome image"/>
但是你的 TypeScript JSX 代码呢?错误:
But your TypeScript JSX code? Errors:
functionImage({src,alt}){// Property 'loading' does not exist.return<imgsrc={src}alt={alt}loading="lazy"/>;}
functionImage({src,alt}){// Property 'loading' does not exist.return<imgsrc={src}alt={alt}loading="lazy"/>;}
为了防止这种情况,我们可以使用自己的属性来扩展可用的接口。这个 TypeScript 特性称为声明合并。
To prevent this, we can extend the available interfaces with our own properties. This TypeScript feature is called declaration merging.
创建一个@types文件夹并将jsx.d.ts文件放入其中。更改您的 TypeScript 配置,以便您的编译器选项允许额外的类型:
Create an @types folder and put a jsx.d.ts file in it. Change your TypeScript config so your compiler options allow for extra types:
{"compilerOptions":{.../* Type declaration files to be included in compilation. */"types":["@types/**"],},...}
{"compilerOptions":{.../* Type declaration files to be included in compilation. */"types":["@types/**"],},...}
我们重新创建精确的模块和接口结构:
We re-create the exact module and interface structure:
该模块名为'react'。
The module is called 'react'.
界面是ImgHTMLAttributes<T> extends HTMLAttributes<T>。
The interface is ImgHTMLAttributes<T> extends HTMLAttributes<T>.
我们从原始类型中知道这一点。在这里,我们添加所需的属性:
We know that from the original typings. Here, we add the properties we want:
import"react";declaremodule"react"{interfaceImgHTMLAttributes<T>extendsHTMLAttributes<T>{loading?:"lazy"|"eager"|"auto";}}
import"react";declaremodule"react"{interfaceImgHTMLAttributes<T>extendsHTMLAttributes<T>{loading?:"lazy"|"eager"|"auto";}}
在我们这样做的同时,请确保我们不会忘记替代文本:
And while we are at it, let’s make sure we don’t forget alt texts:
import"react";declaremodule"react"{interfaceImgHTMLAttributes<T>extendsHTMLAttributes<T>{loading?:"lazy"|"eager"|"auto";alt:string;}}
import"react";declaremodule"react"{interfaceImgHTMLAttributes<T>extendsHTMLAttributes<T>{loading?:"lazy"|"eager"|"auto";alt:string;}}
这好多了!TypeScript 将采用原始定义并合并您的声明。您的自动完成功能可以为您提供所有可用选项,并且当您忘记替代文本时会出错。
That’s much better! TypeScript will take the original definition and merge your declarations. Your autocomplete can give you all available options and will error when you forget an alt text.
使用Preact时,事情会变得稍微复杂一些。原始 HTML 类型非常宽泛,不像 React 的类型那么具体。这就是为什么我们在定义图像时必须更明确一些:
When working with Preact, things are a bit more complicated. The original HTML typings are very generous and not as specific as React’s typings. That’s why we have to be a bit more explicit when defining images:
declarenamespaceJSX{interfaceIntrinsicElements{img:HTMLAttributes&{alt:string;src:string;loading?:"lazy"|"eager"|"auto";};}}
declarenamespaceJSX{interfaceIntrinsicElements{img:HTMLAttributes&{alt:string;src:string;loading?:"lazy"|"eager"|"auto";};}}
这确保alt和都src可用,并添加一个名为的新属性loading。但技术是相同的:声明合并,它在命名空间、接口和模块级别上起作用。
This makes sure that both alt and src are available and adds a new attribute called loading. The technique is the same, though: declaration merging, which works on the level of namespaces, interfaces, and modules.
使用自定义类型定义扩充全局命名空间。
Augment the global namespace with custom type definitions.
TypeScript 将所有 DOM API 的类型存储在lib.dom.d.ts中。此文件由 Web IDL 文件自动生成。Web IDL代表Web 接口定义语言,是 W3C 和 WHATWG 用来定义 Web API 接口的格式。它于 2012 年左右问世,自 2016 年以来已成为标准。
TypeScript stores types to all DOM APIs in lib.dom.d.ts. This file is autogenerated from Web IDL files. Web IDL stands for Web Interface Definition Language and is a format the W3C and WHATWG use to define interfaces to web APIs. It came out around 2012 and has been a standard since 2016.
当你阅读W3C的标准(例如Resize Observer)时,你可以在规范中的某个地方看到定义的部分或完整定义。就像这样:
When you read standards at W3C—like on Resize Observer—you can see parts of a definition or the full definition somewhere within the specification. Like this one:
枚举 ResizeObserverBoxOptions {
“边框”、“内容框”、“设备像素内容框”
};
字典 ResizeObserverOptions {
ResizeObserverBoxOptions box = “内容框”;
};
[暴露=(窗口)]
接口 ResizeObserver {
构造函数(ResizeObserverCallback回调);
void observer(元素目标,可选的ResizeObserverOptions选项);
void unobserve(元素目标);
无效断开连接();
};
回调 ResizeObserverCallback = void (
序列<ResizeObserverEntry>条目,
ResizeObserver 观察者
(英文):
[暴露=窗口]
接口 ResizeObserverEntry {
只读属性元素目标;
只读属性 DOMRectReadOnly contentRect;
只读属性FrozenArray<ResizeObserverSize> borderBoxSize;
只读属性 FrozenArray<ResizeObserverSize> contentBoxSize;
只读属性FrozenArray<ResizeObserverSize> devicePixelContentBoxSize;
};
接口 ResizeObserverSize {
只读属性不受限制双内联大小;
只读属性不受限制双倍块大小;
};
接口 ResizeObservation {
构造函数(元素目标);
只读属性元素目标;
只读属性 ResizeObserverBoxOptions observerBox;
只读属性 FrozenArray<ResizeObserverSize> lastReportedSizes;
};enum ResizeObserverBoxOptions {
"border-box", "content-box", "device-pixel-content-box"
};
dictionary ResizeObserverOptions {
ResizeObserverBoxOptions box = "content-box";
};
[Exposed=(Window)]
interface ResizeObserver {
constructor(ResizeObserverCallback callback);
void observe(Element target, optional ResizeObserverOptions options);
void unobserve(Element target);
void disconnect();
};
callback ResizeObserverCallback = void (
sequence<ResizeObserverEntry> entries,
ResizeObserver observer
);
[Exposed=Window]
interface ResizeObserverEntry {
readonly attribute Element target;
readonly attribute DOMRectReadOnly contentRect;
readonly attribute FrozenArray<ResizeObserverSize> borderBoxSize;
readonly attribute FrozenArray<ResizeObserverSize> contentBoxSize;
readonly attribute FrozenArray<ResizeObserverSize> devicePixelContentBoxSize;
};
interface ResizeObserverSize {
readonly attribute unrestricted double inlineSize;
readonly attribute unrestricted double blockSize;
};
interface ResizeObservation {
constructor(Element target);
readonly attribute Element target;
readonly attribute ResizeObserverBoxOptions observedBox;
readonly attribute FrozenArray<ResizeObserverSize> lastReportedSizes;
};
浏览器以此为指导来实现相应的 API。TypeScript 使用这些 IDL 文件来生成lib.dom.d.ts。TypeScript和 JavaScript lib 生成器项目会抓取 Web 标准并提取 IDL 信息。然后,IDL 到 TypeScript生成器会解析 IDL 文件并生成正确的类型。
Browsers use this as a guideline to implement respective APIs. TypeScript uses these IDL files to generate lib.dom.d.ts. The TypeScript and JavaScript lib generator project scrapes web standards and extracts IDL information. Then an IDL to TypeScript generator parses the IDL file and generates the correct typings.
ResizeObserver要抓取的页面是手动维护的。一旦规范足够完善并得到所有主流浏览器的支持,人们就会添加新资源,并在即将发布的 TypeScript 版本中看到他们的更改。因此,我们进入lib.dom.d.ts只是时间问题。
Pages to scrape are maintained manually. The moment a specification is far enough and supported by all major browsers, people add a new resource and see their change released with an upcoming TypeScript version. So it’s just a matter of time until we get ResizeObserver in lib.dom.d.ts.
如果我们等不及,我们可以自己添加类型,但仅限于我们目前正在进行的项目。
If we can’t wait, we can add the typings ourselves but only for the project we currently are working with.
假设我们生成了 的类型ResizeObserver。我们将输出存储在名为resize-observer.d.ts的文件中。内容如下:
Let’s assume we generated the types for ResizeObserver. We would store the output in a file called resize-observer.d.ts. Here are the contents:
typeResizeObserverBoxOptions="border-box"|"content-box"|"device-pixel-content-box";interfaceResizeObserverOptions{box?:ResizeObserverBoxOptions;}interfaceResizeObservation{readonlylastReportedSizes:ReadonlyArray<ResizeObserverSize>;readonlyobservedBox:ResizeObserverBoxOptions;readonlytarget:Element;}declarevarResizeObservation:{prototype:ResizeObservation;new(target:Element):ResizeObservation;};interfaceResizeObserver{disconnect():void;observe(target:Element,options?:ResizeObserverOptions):void;unobserve(target:Element):void;}exportdeclarevarResizeObserver:{prototype:ResizeObserver;new(callback:ResizeObserverCallback):ResizeObserver;};interfaceResizeObserverEntry{readonlyborderBoxSize:ReadonlyArray<ResizeObserverSize>;readonlycontentBoxSize:ReadonlyArray<ResizeObserverSize>;readonlycontentRect:DOMRectReadOnly;readonlydevicePixelContentBoxSize:ReadonlyArray<ResizeObserverSize>;readonlytarget:Element;}declarevarResizeObserverEntry:{prototype:ResizeObserverEntry;new():ResizeObserverEntry;};interfaceResizeObserverSize{readonlyblockSize:number;readonlyinlineSize:number;}declarevarResizeObserverSize:{prototype:ResizeObserverSize;new():ResizeObserverSize;};interfaceResizeObserverCallback{(entries:ResizeObserverEntry[],observer:ResizeObserver):void;}
typeResizeObserverBoxOptions="border-box"|"content-box"|"device-pixel-content-box";interfaceResizeObserverOptions{box?:ResizeObserverBoxOptions;}interfaceResizeObservation{readonlylastReportedSizes:ReadonlyArray<ResizeObserverSize>;readonlyobservedBox:ResizeObserverBoxOptions;readonlytarget:Element;}declarevarResizeObservation:{prototype:ResizeObservation;new(target:Element):ResizeObservation;};interfaceResizeObserver{disconnect():void;observe(target:Element,options?:ResizeObserverOptions):void;unobserve(target:Element):void;}exportdeclarevarResizeObserver:{prototype:ResizeObserver;new(callback:ResizeObserverCallback):ResizeObserver;};interfaceResizeObserverEntry{readonlyborderBoxSize:ReadonlyArray<ResizeObserverSize>;readonlycontentBoxSize:ReadonlyArray<ResizeObserverSize>;readonlycontentRect:DOMRectReadOnly;readonlydevicePixelContentBoxSize:ReadonlyArray<ResizeObserverSize>;readonlytarget:Element;}declarevarResizeObserverEntry:{prototype:ResizeObserverEntry;new():ResizeObserverEntry;};interfaceResizeObserverSize{readonlyblockSize:number;readonlyinlineSize:number;}declarevarResizeObserverSize:{prototype:ResizeObserverSize;new():ResizeObserverSize;};interfaceResizeObserverCallback{(entries:ResizeObserverEntry[],observer:ResizeObserver):void;}
我们声明了大量的接口和一些实现接口的变量,例如declare var ResizeObserver,它是定义原型和构造函数的对象:
We declare a ton of interfaces and some variables that implement our interfaces, like declare var ResizeObserver, which is the object that defines the prototype and constructor function:
declarevarResizeObserver:{prototype:ResizeObserver;new(callback:ResizeObserverCallback):ResizeObserver;};
declarevarResizeObserver:{prototype:ResizeObserver;new(callback:ResizeObserverCallback):ResizeObserver;};
这已经很有帮助了。我们可以使用(可以说是)长类型声明,并将它们直接放在我们需要的文件中。ResizeObserver找到了!不过,我们希望它在任何地方都可用。
This already helps a lot. We can use the (arguably) long type declarations and put them directly in the file where we need them. ResizeObserver is found! We want to have it available everywhere, though.
借助 TypeScript 的声明合并功能,我们可以根据需要扩展命名空间和接口。这次,我们扩展的是全局命名空间。
Thanks to TypeScript’s declaration-merging feature, we can extend namespaces and interfaces as needed. This time, we’re extending the global namespace.
全局命名空间包含所有全局可用的对象和接口。例如window对象(和Window接口),以及应该成为 JavaScript 执行上下文一部分的所有其他内容。我们扩充全局命名空间并将对象添加ResizeObserver到其中:
The global namespace contains all objects and interfaces that are, well, globally available. Like the window object (and Window interface), as well as everything else that should be part of our JavaScript execution context. We augment the global namespace and add the ResizeObserver object to it:
declareglobal{// opening up the namespacevarResizeObserver:{// merging ResizeObserver with itprototype:ResizeObserver;new(callback:ResizeObserverCallback):ResizeObserver;}}
declareglobal{// opening up the namespacevarResizeObserver:{// merging ResizeObserver with itprototype:ResizeObserver;new(callback:ResizeObserverCallback):ResizeObserver;}}
我们将resize-observer.d.ts放在名为@types的文件夹中。不要忘记将该文件夹添加到 TypeScript 将解析的源以及tsconfig.json中的类型声明文件夹列表中:
Let’s put resize-observer.d.ts in a folder called @types. Don’t forget to add the folder to the sources that TypeScript will parse as well as the list of type declaration folders in tsconfig.json:
{"compilerOptions":{//..."typeRoots":["@types","./node_modules/@types"],//...},"include":["src","@types"]}
{"compilerOptions":{//..."typeRoots":["@types","./node_modules/@types"],//...},"include":["src","@types"]}
由于ResizeObserver目标浏览器很可能尚未提供该对象,因此请确保创建该ResizeObserver对象undefined。这促使您检查该对象是否可用:
Since there’s a significant possibility that ResizeObserver is not yet available in your target browser, make sure that you make the ResizeObserver object undefined. This urges you to check if the object is available:
declareglobal{varResizeObserver:{prototype:ResizeObserver;new(callback:ResizeObserverCallback):ResizeObserver;}|undefined}
declareglobal{varResizeObserver:{prototype:ResizeObserver;new(callback:ResizeObserverCallback):ResizeObserver;}|undefined}
在您的应用程序中:
In your application:
if(typeofResizeObserver!=='undefined'){constx=newResizeObserver((entries)=>{});}
if(typeofResizeObserver!=='undefined'){constx=newResizeObserver((entries)=>{});}
这使得工作ResizeObserver尽可能安全!
This makes working with ResizeObserver as safe as possible!
可能是 TypeScript 无法获取您的环境声明文件和全局增强。如果发生这种情况,请确保:
It might be that TypeScript doesn’t pick up your ambient declaration files and the global augmentation. If this happens, make sure that:
您可以通过tsconfig.json中的属性解析@types文件夹。include
You parse the @types folder via the include property in tsconfig.json.
通过将环境类型声明文件添加到tsconfig.jsontypes编译器选项中或typeRoots在其中添加,就可以识别它们。
Your ambient type declaration files are recognized as such by adding them to types or typeRoots in the tsconfig.json compiler options.
You add export {} at the end of your ambient declaration file so TypeScript recognizes this file as a module.
根据文件扩展名全局声明模块。
Globally declare modules based on filename extensions.
在 Web 开发中,有一种趋势是将 JavaScript 设为所有内容的默认入口点,并让它通过import语句处理所有相关资产。为此,您需要一个构建工具,一个打包器,它可以分析您的代码并创建正确的工件。一个流行的工具是Webpack,这是一个 JavaScript 打包器,允许您打包所有内容— CSS、Markdown、SVG、JPEG,应有尽有:
There is a movement in web development to make JavaScript the default entry point of everything and let it handle all relevant assets via import statements. What you need for this is a build tool, a bundler, that analyzes your code and creates the right artifacts. A popular tool for this is Webpack, a JavaScript bundler that allows you to bundle everything—CSS, Markdown, SVGs, JPEGs, you name it:
// like thisimport"./Button.css";// or thisimportstylesfrom"./Button.css";
// like thisimport"./Button.css";// or thisimportstylesfrom"./Button.css";
Webpack 使用一种称为loader 的概念,它查看文件结尾并激活某些捆绑概念。在 JavaScript 中导入.css文件不是原生的。它是 Webpack(或您正在使用的任何捆绑器)的一部分。但是,我们可以教 TypeScript 理解这样的文件。
Webpack uses a concept called loaders, which looks at file endings and activates certain bundling concepts. Importing .css files in JavaScript is not native. It’s part of Webpack (or whatever bundler you are using). However, we can teach TypeScript to understand files like this.
ECMAScript 标准委员会提出了一项提案,允许导入除 JavaScript 之外的文件,并为此声明某些内置格式。这最终会对 TypeScript 产生影响。您可以在此处阅读有关它的全部内容。
There is a proposal in the ECMAScript standards committee to allow imports of files other than JavaScript and assert certain built-in formats for this. This will have an effect on TypeScript eventually. You can read all about it here.
TypeScript 支持环境模块声明,即使对于不“物理”存在但在环境中或可通过工具访问的模块也是如此。一个示例是 Node 的主要内置模块,例如url或http,path如 TypeScript 文档中所述:
TypeScript supports ambient module declarations, even for a module that is not “physically” there but in the environment or reachable via tooling. One example is Node’s main built-in modules, like url, http or path, as described in TypeScript’s documentation:
declaremodule"path"{exportfunctionnormalize(p:string):string;exportfunctionjoin(...paths:any[]):string;exportvarsep:string;}
declaremodule"path"{exportfunctionnormalize(p:string):string;exportfunctionjoin(...paths:any[]):string;exportvarsep:string;}
对于我们知道确切名称的模块,这非常有用。我们还可以将相同的技术用于通配符模式。让我们为所有.css文件声明一个通用环境模块:
This is great for modules where we know the exact name. We can also use the same technique for wildcard patterns. Let’s declare a generic ambient module for all our .css files:
declaremodule'*.css'{// to be done.}
declaremodule'*.css'{// to be done.}
模式已准备就绪。这将监听我们要导入的所有.css文件。我们期望的是可以添加到组件中的类名列表。由于我们不知道.css文件中定义了哪些类,因此我们使用一个接受每个字符串键并返回字符串的对象:
The pattern is ready. This listens to all .css files we want to import. What we expect is a list of class names that we can add to our components. Since we don’t know which classes are defined in the .css files, let’s go with an object that accepts every string key and returns a string:
declaremodule'*.css'{interfaceIClassNames{[className:string]:string}constclassNames:IClassNames;exportdefaultclassNames;}
declaremodule'*.css'{interfaceIClassNames{[className:string]:string}constclassNames:IClassNames;exportdefaultclassNames;}
这就是我们再次编译文件所需的全部内容。唯一的缺点是我们不能使用确切的类名来获得自动完成和类似的好处。解决这个问题的一种方法是自动生成类型文件。NPM 上有一些包可以解决这个问题。请随意选择你喜欢的一个。
That’s all we need to make our files compile again. The only downside is that we can’t use the exact class names to get autocompletion and similar benefits. A way to solve this is to generate type files automatically. There are packages on NPM that deal with that problem. Feel free to choose one of your liking.
如果我们想将 MDX 之类的东西导入到我们的模块中,那就简单多了。MDX 让我们可以编写 Markdown,它可以解析为常规的 React(或 JSX)组件(有关 React 的更多信息,请参阅第 10 章)。
It’s a bit easier if we want to import something like MDX into our modules. MDX lets us write Markdown, which parses to regular React (or JSX) components (more on React in Chapter 10).
我们期望一个返回 JSX 元素的功能组件(我们可以向其传递 props):
We expect a functional component (that we can pass props to) that returns a JSX element:
declaremodule'*.mdx'{letMDXComponent:(props)=>JSX.Element;exportdefaultMDXComponent;}
declaremodule'*.mdx'{letMDXComponent:(props)=>JSX.Element;exportdefaultMDXComponent;}
好了!我们可以在 JavaScript 中加载.mdx文件并将其用作组件:
And voilà! We can load .mdx files in JavaScript and use them as components:
importAboutfrom'../articles/about.mdx';functionApp(){return<><About/></>}
importAboutfrom'../articles/about.mdx';functionApp(){return<><About/></>}
如果你不知道会发生什么,那就让你的生活变得轻松。你需要做的就是声明模块。不要提供任何类型。TypeScript 将允许加载,但不会给你任何类型安全:
If you don’t know what to expect, make your life easy. All you need to do is declare the module. Don’t provide any types. TypeScript will allow loading but won’t give you any type safety:
declaremodule'*.svg';
declaremodule'*.svg';
为了使环境模块可用于您的应用,建议在项目中的某个位置(可能是根级别)创建一个@types文件夹。在那里,您可以放置任意数量的包含模块定义的.d.ts文件。向您的tsconfig.json添加引用,TypeScript 就知道该做什么了:
To make ambient modules available to your app, it is recommended to create an @types folder somewhere in your project (probably root level). There you can put any amount of .d.ts files with your module definitions. Add a referral to your tsconfig.json, and TypeScript knows what to do:
{..."compilerOptions":{..."typeRoots":["./node_modules/@types","./@types"],...}}
{..."compilerOptions":{..."typeRoots":["./node_modules/@types","./@types"],...}}
TypeScript 的主要功能之一是能够适应所有 JavaScript 风格。有些功能是内置的,有些则需要您进行一些额外的修补。
One of TypeScript’s main features is to be adaptable to all JavaScript flavors. Some things are built-in, and others need some extra patching from you.
1当 API 定义被创建时,它unknown并不存在。此外,TypeScript 非常注重 开发人员的生产力,作为res.json()一种广泛使用的方法,这会 破坏无数应用程序。
1 Back when the API defintiion was created, unknown didn’t exist. Also, TypeScript has a strong focus on developer productivity, and with res.json() being a widely used method, this would’ve broken countless applications.
2感谢 Dan Vanderkam 的Effective TypeScript博客为本 主题提供的启发。
2 Credit to Dan Vanderkam’s Effective TypeScript blog for inspiration on this subject.
React 可以说是近年来最流行的 JavaScript 库之一。它简单的组件组合方法改变了我们编写前端(以及在一定程度上后端)应用程序的方式,使您可以使用名为 JSX 的 JavaScript 语法扩展以声明方式编写 UI 代码。这个简单的原则不仅易于掌握和理解,而且还影响了数十个其他库。
React is arguably one of the most popular JavaScript libraries in recent years. Its simple approach to the composition of components has changed the way we write frontend (and, to an extent, backend) applications, allowing you to declaratively write UI code using a JavaScript syntax extension called JSX. Not only was this simple principle easy to pick up and understand, but it also influenced dozens of other libraries.
JSX 无疑是 JavaScript 世界中的游戏规则改变者,而 TypeScript 的目标是满足所有 JavaScript 开发人员的需求,因此 JSX 也进入了 TypeScript。事实上,TypeScript 是一个功能齐全的 JSX 编译器。如果您不需要额外的捆绑或额外的工具,TypeScript 就是您运行 React 应用程序所需的一切。TypeScript 也非常受欢迎。在撰写本文时,NPM 上的 React 类型每周下载量达到 2000 万次。VS Code 的出色工具和出色的类型使 TypeScript 成为全球 React 开发人员的首选。
JSX is undoubtedly a game changer in the JavaScript world, and with TypeScript’s goal to cater to all JavaScript developers, JSX found its way into TypeScript. In fact, TypeScript is a full-fledged JSX compiler. If you have no need for additional bundling or extra tooling, TypeScript is all you need to get your React app going. TypeScript is also immensely popular. At the time of writing, the React typings on NPM clocked 20 million downloads per week. The fantastic tooling with VS Code and the excellent types made TypeScript the first choice for React developers around the globe.
虽然 TypeScript 在 React 开发者中的流行度有增无减,但有一种情况使在 React 中使用 TypeScript 有点困难:TypeScript 不是 React 团队的首选。虽然其他基于 JSX 的库现在大多用TypeScript 编写,因此提供了开箱即用的优秀类型,但 React 团队使用他们自己的静态类型检查器Flow,它与 TypeScript 类似,但最终与 TypeScript 不兼容。这意味着数百万开发人员所依赖的 React 类型随后由一群社区贡献者制作并发布在 Definitely Typed 上。虽然@types/react被认为是优秀的,但它们仍然只是对像 React 这样复杂的库进行类型化的最佳努力。这不可避免地会导致差距。对于这些差距变得明显的地方,本章将为您提供指南。
While TypeScript’s popularity among React developers continues unabated, one circumstance makes the use of TypeScript with React a bit difficult: TypeScript isn’t the React team’s first choice. While other JSX-based libraries are now mostly written in TypeScript and therefore provide excellent types out of the box, the React team works with their own static type-checker called Flow, which is similar to, but ultimately incompatible with, TypeScript. This means the React types millions of developers rely on are made subsequently by a group of community contributors and published on Definitely Typed. While @types/react are considered to be excellent, they are still just the best effort to type a library as complex as React. This inevitably leads to gaps. For the places where those gaps become visible, this chapter will be your guide.
在本章中,我们将讨论 React 应该很容易但 TypeScript 却抛出复杂错误消息的情况。我们将弄清楚这些消息的含义、如何解决它们以及从长远来看哪些解决方案对您有帮助。您还将了解各种开发模式及其优势,以及如何使用 TypeScript 的内置 JSX 支持。
In this chapter, we look at situations where React is supposed to be easy, but TypeScript gives you a hard time by throwing complex error messages. We are going to figure out what those messages mean, how you can work around them, and what solutions help you in the long run. You will also learn about various development patterns and their benefits, and how to use TypeScript’s built-in JSX support.
您不会获得 React 和 TypeScript 的基本设置指南。生态系统如此庞大和丰富,条条大路通罗马。选择您框架的文档页面并留意 TypeScript。另请注意,我假设您事先有一些 React 经验。在本章中,我们主要讨论 React 的类型。
What you won’t get is a basic setup guide for React and TypeScript. The ecosystem is so vast and rich, many roads lead to Rome. Pick your framework’s documentation pages and look out for TypeScript. Also note that I assume some React experience up front. In this chapter, we deal mostly with typing React.
虽然本章强烈倾向于 React,但您将能够使用某些学习内容并将其应用于其他基于 JSX 的框架和库。
While there is a strong inclination toward React in this chapter, you will be able to use certain learnings and apply them to other JSX-based frameworks and libraries as well.
创建代理组件并应用一些模式,使其适用于您的 场景。
Create proxy components and apply a few patterns to make them usable for your scenario.
大多数 Web 应用程序都会使用按钮。按钮具有type默认为 的属性
submit。对于通过 HTTP 执行操作(将内容发布到服务器端 API)的表单,这是一个合理的默认值。但是,当您只想在网站上添加交互元素时,按钮的正确类型是button。这不仅是一种美学选择,而且对于可访问性也很重要:
Most web applications use buttons. Buttons have a type property that defaults to
submit. This is a sensible default for forms where you perform an action over HTTP, where you POST the contents to a server-side API. But when you just want to have interactive elements on your site, the correct type for buttons is button. This is not only an aesthetic choice but also important for accessibility:
<buttontype="button">点击我!</button>
<buttontype="button">Click me!</button>
编写 React 时,您很少会向服务器提交带有类型的表单
submit,但会与许多button-type 按钮进行交互。处理此类情况的一个好方法是编写代理组件。它们模仿 HTML 元素,但预设了几个属性:
When you write React, chances are you rarely submit a form to a server with a
submit type, but you interact with lots of button-type buttons. A good way to deal with situations like these is to write proxy components. They mimic HTML elements but preset a couple of properties:
functionButton(props){return<buttontype="button"{...props}/>;}
functionButton(props){return<buttontype="button"{...props}/>;}
其理念是Button采用与 HTML 相同的属性button,并将属性分散到 HTML 元素中。将属性分散到 HTML 元素是一项很好的功能,您可以确保能够设置元素具有的所有 HTML 属性,而无需事先知道要设置哪些属性。但我们如何输入它们呢?
The idea is that Button takes the same properties as the HTML button, and the attributes are spread out to the HTML element. Spreading attributes to HTML elements is a nice feature where you can make sure that you are able to set all the HTML properties that an element has without knowing up front which you want to set. But how do we type them?
所有可以在 JSX 中使用的 HTML 元素都是通过JSX命名空间中的固有元素定义的。加载 React 时,JSX命名空间会作为全局命名空间出现在您的文件中,您可以通过索引访问来访问所有元素。因此正确的 prop 类型Button在 中定义JSX.IntrinsicElements。
All HTML elements that can be used in JSX are defined through intrinsic elements in the JSX namespace. When you load React, the JSX namespace appears as a global namespace in your file, and you can access all elements via index access. So the correct prop types for Button are defined in JSX.IntrinsicElements.
的替代方案JSX.IntrinsicElements是React.ElementTypeReact 包中的泛型类型,其中还包括类和函数组件。对于代理组件,JSX.IntrinsicElements已经足够,并且还具有额外的好处:您的组件与其他类似 React 的框架(如
Preact)保持兼容。
An alternative to JSX.IntrinsicElements is React.ElementType, a generic type within the React package, which also includes class and function components. For proxy components, JSX.IntrinsicElements is sufficient and comes with an extra benefit: your components stay compatible with other React-like frameworks like
Preact.
JSX.IntrinsicElements是全局命名空间中的类型JSX。一旦此命名空间处于范围内,TypeScript 就能够选择与基于 JSX 的框架兼容的基本元素:
JSX.IntrinsicElements is a type within the global JSX namespace. Once this namespace is in scope, TypeScript is able to pick up basic elements that are compatible with your JSX-based framework:
typeButtonProps=JSX.IntrinsicElements["button"];functionButton(props:ButtonProps){return<buttontype="button"{...props}/>;}
typeButtonProps=JSX.IntrinsicElements["button"];functionButton(props:ButtonProps){return<buttontype="button"{...props}/>;}
这包括子元素:我们将它们分散开来!如您所见,我们将按钮的类型设置为button。由于 props 只是 JavaScript 对象,因此可以type通过将其设置为 props 中的属性来覆盖。如果定义了两个同名的键,则最后一个键获胜。这可能是所需的行为,但您可能希望阻止您和您的同事覆盖type。使用Omit<T, K>辅助类型,您可以从 JSX 中获取所有属性button,但删除您不想覆盖的键:
This includes children: we spread them along! As you see, we set a button’s type to button. Since props are just JavaScript objects, it’s possible to override type by setting it as an attribute in props. If two keys with the same name are defined, the last one wins. This may be desired behavior, but you alternatively may want to prevent you and your colleagues from overriding type. With the Omit<T, K> helper type, you can take all properties from a JSX button but drop keys you don’t want to override:
typeButtonProps=Omit<JSX.IntrinsicElements["button"],"type">;functionButton(props:ButtonProps){return<buttontype="button"{...props}/>;}constaButton=<Buttontype="button">Hi</Button>;// ^// Type '{ children: string; type: string; }' is not// assignable to type 'IntrinsicAttributes & ButtonProps'.// Property 'type' does not exist on type// 'IntrinsicAttributes & ButtonProps'.(2322)
typeButtonProps=Omit<JSX.IntrinsicElements["button"],"type">;functionButton(props:ButtonProps){return<buttontype="button"{...props}/>;}constaButton=<Buttontype="button">Hi</Button>;// ^// Type '{ children: string; type: string; }' is not// assignable to type 'IntrinsicAttributes & ButtonProps'.// Property 'type' does not exist on type// 'IntrinsicAttributes & ButtonProps'.(2322)
如果需要type,submit您可以创建另一个代理组件:
If you need type to be submit, you can create another proxy component:
typeSubmitButtonProps=Omit<JSX.IntrinsicElements["button"],"type">;functionSubmitButton(props:SubmitButtonProps){return<buttontype="submit"{...props}/>;}
typeSubmitButtonProps=Omit<JSX.IntrinsicElements["button"],"type">;functionSubmitButton(props:SubmitButtonProps){return<buttontype="submit"{...props}/>;}
如果想要预设更多属性,可以扩展省略属性的想法。也许你遵循设计系统,不想随意设置类名:
You can extend this idea of omitting properties if you want to preset even more properties. Perhaps you adhere to a design system and don’t want class names to be set arbitrarily:
typeStyledButton=Omit<JSX.IntrinsicElements["button"],"type"|"className"|"style">&{type:"primary"|"secondary";};functionStyledButton({type,...allProps}:StyledButton){return<Buttontype="button"className={`btn-${type}`}{...allProps}/>;}
typeStyledButton=Omit<JSX.IntrinsicElements["button"],"type"|"className"|"style">&{type:"primary"|"secondary";};functionStyledButton({type,...allProps}:StyledButton){return<Buttontype="button"className={`btn-${type}`}{...allProps}/>;}
This even allows you to reuse the type property name.
我们从类型定义中删除了一些 props,并将它们预设为合理的默认值。现在我们要确保用户不会忘记设置一些 props,例如alt图像的属性或src属性。
We dropped some props from the type definition and preset them to sensible defaults. Now we want to make sure our users don’t forget to set some props, such as the alt attribute of an image or the src attribute.
为此,我们创建一个MakeRequired删除可选标志的辅助类型:
For that, we create a MakeRequired helper type that removes the optional flag:
typeMakeRequired<T,KextendskeyofT>=Omit<T,K>&Required<Pick<T,K>;
typeMakeRequired<T,KextendskeyofT>=Omit<T,K>&Required<Pick<T,K>;
并构建我们自己的道具:
And build our own props:
typeImgProps=MakeRequired<JSX.IntrinsicElements["img"],"alt"|"src">;exportfunctionImg(props:ImgProps){return<img{...props}/>;}constanImage=<Img/>;// ^// Type '{}' is missing the following properties from type// 'Required<Pick<DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>,// HTMLImageElement>, "alt" | "src">>': alt, src (2739)
typeImgProps=MakeRequired<JSX.IntrinsicElements["img"],"alt"|"src">;exportfunctionImg(props:ImgProps){return<img{...props}/>;}constanImage=<Img/>;// ^// Type '{}' is missing the following properties from type// 'Required<Pick<DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>,// HTMLImageElement>, "alt" | "src">>': alt, src (2739)
只需对原始内在元素的类型和代理组件进行一些更改,我们就可以确保我们的代码变得更加健壮、更易于访问且更不容易出错。
With just a few changes to the original intrinsic element’s type and a proxy component, we can ensure that our code becomes more robust, more accessible, and less error prone.
编写一个使用可区分联合和可选的 never 技术的代理组件,以确保您不会在运行时从不受控制切换到受控制。
Write a proxy component that uses discriminated unions and the optional never technique to ensure you won’t switch from uncontrolled to controlled at runtime.
React 将表单元素分为受控组件和非受控组件。当您使用常规表单元素(如input、textarea或select)时,需要记住底层 HTML 元素控制其自己的状态。而在 React 中,元素的状态也是通过React 定义的。
React differentiates form elements between controlled components and uncontrolled components. When you use regular form elements like input, textarea, or select, you need to keep in mind that the underlying HTML elements control their own state. Whereas in React, the state of an element is also defined through React.
如果你设置了该属性,React 会假定该元素的值也由 React 的状态管理控制,这意味着除非你使用和相关的 setter
函数value维护元素的状态,否则你无法修改该值。useState
If you set the value attribute, React assumes that the element’s value is also controlled by React’s state management, which means you are not able to modifiy this value unless you maintain the element’s state using useState and the associated setter
function.
有两种方法可以解决这个问题。首先,您可以选择defaultValue作为属性而不是value。这将value仅在第一次渲染时设置输入的,随后将所有内容都交给浏览器:
There are two ways to deal with this. First, you can choose defaultValue as a property instead of value. This will set the value of the input only in the first rendering, and subsequently leaves everything in the hands of the browser:
functionInput({value="",...allProps}:Props){return(<inputdefaultValue={value}{...allProps}/>);}
functionInput({value="",...allProps}:Props){return(<inputdefaultValue={value}{...allProps}/>);}
或者你value通过 React 的状态管理进行内部管理。通常,只需将原始输入元素的 props 与我们自己的类型相交就足够了。我们value从内部元素中删除并将其添加为必需元素string:
Or you manage value interally via React’s state management. Usually, it’s enough just to intersect the original input element’s props with our own type. We drop value from the intrinsic elements and add it as a required string:
typeControlledProps=Omit<JSX.IntrinsicElements["input"],"value">&{value:string;};
typeControlledProps=Omit<JSX.IntrinsicElements["input"],"value">&{value:string;};
然后,我们将input元素包装在代理组件中。在代理组件中内部保存状态并不是最佳实践;相反,你应该使用从外部管理它useState。我们还转发了onChange从原始输入 props 传递的处理程序:
Then, we wrap the input element in a proxy component. It is not best practice to keep state internally in a proxy component; rather, you should manage it from the outside with useState. We also forward the onChange handler we pass from the original input props:
functionInput({value="",onChange,...allProps}:ControlledProps){return(<inputvalue={value}{...allProps}onChange={onChange}/>);}functionAComponentUsingInput(){const[val,setVal]=useState("");return<Inputvalue={val}onChange={(e)=>{setVal(e.target.value);}}/>}
functionInput({value="",onChange,...allProps}:ControlledProps){return(<inputvalue={value}{...allProps}onChange={onChange}/>);}functionAComponentUsingInput(){const[val,setVal]=useState("");return<Inputvalue={val}onChange={(e)=>{setVal(e.target.value);}}/>}
React 在运行时从不受控切换到受控时会引发一个有趣的警告:
React raises an interesting warning when dealing with a switch from uncontrolled to controlled at runtime:
组件正在将不受控的输入更改为受控的输入。这可能是由于值从未定义更改为已定义值而导致的,这种情况不应该发生。决定在组件的整个生命周期内使用受控或不受控的输入元素。
A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
我们可以通过在编译时确保始终提供已定义的字符串value或提供替代字符串(但不能同时提供两者)来避免出现此警告。这可以通过使用可选 never 技术(如示例 3.8所示)的可区分联合类型以及示例 8.1中的辅助类型从中派生可能的属性来defaultValue解决:OnlyRequiredJSX.IntrinsicElements["input"]
We can prevent this warning by making sure at compile time that we either always provide a defined string value or provide a defaultValue instead, but not both. This can be solved by using a discriminated union type using the optional never technique (as seen in Recipe 3.8), and using the OnlyRequired helper type from Recipe 8.1 to derive possible properties from JSX.IntrinsicElements["input"]:
importReact,{useState}from"react";// A helper type setting a few properties to be requiredtypeOnlyRequired<T,KextendskeyofT=keyofT>=Required<Pick<T,K>>&Partial<Omit<T,K>>;// Branch 1: Make "value" and "onChange" required, drop `defaultValue`typeControlledProps=OnlyRequired<JSX.IntrinsicElements["input"],"value"|"onChange">&{defaultValue?:never;};// Branch 2: Drop `value` and `onChange`, make `defaultValue` requiredtypeUncontrolledProps=Omit<JSX.IntrinsicElements["input"],"value"|"onChange">&{defaultValue:string;value?:never;onChange?:never;};typeInputProps=ControlledProps|UncontrolledProps;functionInput({...allProps}:InputProps){return<input{...allProps}/>;}functionControlled(){const[val,setVal]=useState("");return<Inputvalue={val}onChange={(e)=>setVal(e.target.value)}/>;}functionUncontrolled(){return<InputdefaultValue="Hello"/>;}
importReact,{useState}from"react";// A helper type setting a few properties to be requiredtypeOnlyRequired<T,KextendskeyofT=keyofT>=Required<Pick<T,K>>&Partial<Omit<T,K>>;// Branch 1: Make "value" and "onChange" required, drop `defaultValue`typeControlledProps=OnlyRequired<JSX.IntrinsicElements["input"],"value"|"onChange">&{defaultValue?:never;};// Branch 2: Drop `value` and `onChange`, make `defaultValue` requiredtypeUncontrolledProps=Omit<JSX.IntrinsicElements["input"],"value"|"onChange">&{defaultValue:string;value?:never;onChange?:never;};typeInputProps=ControlledProps|UncontrolledProps;functionInput({...allProps}:InputProps){return<input{...allProps}/>;}functionControlled(){const[val,setVal]=useState("");return<Inputvalue={val}onChange={(e)=>setVal(e.target.value)}/>;}functionUncontrolled(){return<InputdefaultValue="Hello"/>;}
在所有其他情况下,类型系统将禁止具有可选项value或具有并尝试控制值。defaultValue
In all other cases, having an optional value or having a defaultValue and trying to control values will be prohibited by the type system.
使用元组类型或const context。
Use tuple types or const context.
让我们在 React 中创建一个自定义钩子,并遵循常规 React 钩子的命名约定:返回一个可以解构的数组(或元组)。例如useState:
Let’s create a custom hook in React and stick to the naming convention as regular React hooks do: returning an array (or tuple) that can be destructured. For example, useState:
const[state,setState]=useState(0);
const[state,setState]=useState(0);
我们为什么要使用数组?因为数组的字段没有名称,你可以设置自己的名称:
Why do we even use arrays? Because the array’s fields have no name, and you can set names of your own:
const[count,setCount]=useState(0);const[darkMode,setDarkMode]=useState(true);
const[count,setCount]=useState(0);const[darkMode,setDarkMode]=useState(true);
因此,如果您有类似的模式,您自然也希望返回一个数组。自定义切换钩子可能如下所示:
So naturally, if you have a similar pattern, you also want to return an array. A custom toggle hook might look like this:
exportconstuseToggle=(initialValue:boolean)=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);return[value,toggleValue];}
exportconstuseToggle=(initialValue:boolean)=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);return[value,toggleValue];}
没什么特别的。我们唯一需要设置的类型是输入参数的类型。让我们尝试一下:
Nothing out of the ordinary. The only types we have to set are the types of the input parameters. Let’s try it:
exportconstBody=()=>{const[isVisible,toggleVisible]=useToggle(false)return(<><buttononClick={toggleVisible}></button>{/* Error. See below */}{isVisible&&<div>世界</div>}>}</>)}// Error: Type 'boolean | (() => void)' is not assignable to// type 'MouseEventHandler<HTMLButtonElement> | undefined'.// Type 'boolean' is not assignable to type// 'MouseEventHandler<HTMLButtonElement>'.(2322)
exportconstBody=()=>{const[isVisible,toggleVisible]=useToggle(false)return(<><buttononClick={toggleVisible}></button>{/* Error. See below */}{isVisible&&<div>World</div>}>}</>)}// Error: Type 'boolean | (() => void)' is not assignable to// type 'MouseEventHandler<HTMLButtonElement> | undefined'.// Type 'boolean' is not assignable to type// 'MouseEventHandler<HTMLButtonElement>'.(2322)
那么为什么会失败呢?错误信息可能很隐晦,但我们应该注意第一个类型,它被声明为不兼容:boolean | (() => void)'。这来自返回一个数组:一个任意长度的列表,可以容纳尽可能多的元素。从中的返回值useToggle,TypeScript 推断出一个数组类型。由于的类型value是boolean(太棒了!)和的类型toggleValue是(() => void)(一个预期不返回任何内容的函数),TypeScript 告诉我们在这个数组中两种类型都是可能的。
So why does this fail? The error message might be cryptic, but what we should look out for is the first type, which is declared incompatible: boolean | (() => void)'. This comes from returning an array: a list of any length that can hold as many elements as virtually possible. From the return value in useToggle, TypeScript infers an array type. Since the type of value is boolean (great!) and the type of toggleValue is (() => void) (a function expected to return nothing), TypeScript tells us that both types are possible in this array.
这就是破坏兼容性的原因onClick。onClick期望一个函数。 这没问题,但是toggleValue(或toggleVisible) 是一个函数。 但是,根据 TypeScript,它也可以是布尔值! TypeScript 会告诉您要明确,或者至少要进行类型检查。
This is what breaks the compatibility with onClick. onClick expects a function. That’s fine, but toggleValue (or toggleVisible) is a function. According to TypeScript, however, it can also be a Boolean! TypeScript tells you to be explicit, or at least to do type-checks.
但我们不需要做额外的类型检查。我们的代码非常清晰。错误在于类型。因为我们处理的不是数组,所以我们使用另一个名称:元组。虽然数组是任意长度的值列表,但我们确切地知道元组中有多少个值。通常,我们也知道元组中每个元素的类型。
But we shouldn’t need to do extra type-checks. Our code is very clear. It’s the types that are wrong. Because we’re not dealing with an array, let’s go for a different name: tuple. While an array is a list of values that can be of any length, we know exactly how many values we get in a tuple. Usually, we also know the type of each element in a tuple.
因此,我们不应该返回数组,而应该返回元组useToggle。问题是:在 JavaScript 中,数组和元组是无法区分的。在 TypeScript 的类型系统中,我们可以区分它们。
So we shouldn’t return an array but a tuple at useToggle. The problem: in JavaScript an array and a tuple are indistinguishable. In TypeScript’s type system, we can distinguish them.
第一种选择:让我们有意地使用返回类型。由于 TypeScript 正确地推断出一个数组,因此我们必须告诉 TypeScript 我们期望一个元组:
First option: let’s be intentional with our return type. Since TypeScript—correctly!—infers an array, we have to tell TypeScript that we are expecting a tuple:
// add a return type hereexportconstuseToggle=(initialValue:boolean):[boolean,()=>void]=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);return[value,toggleValue];};
// add a return type hereexportconstuseToggle=(initialValue:boolean):[boolean,()=>void]=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);return[value,toggleValue];};
使用[boolean, () => void]作为返回类型,TypeScript 检查我们是否在此函数中返回元组。TypeScript 不会推断,而是确保您预期的返回类型与实际值匹配。瞧,您的代码不再抛出错误。
With [boolean, () => void] as a return type, TypeScript checks that we are returning a tuple in this function. TypeScript does not infer, but rather makes sure that your intended return type is matched by the actual values. And voilà, your code doesn’t throw errors anymore.
第二种选择:使用const context。有了元组,我们知道需要多少个元素,也知道这些元素的类型。这听起来像是用断言冻结类型的工作const:
Second option: use const context. With a tuple, we know how many elements we are expecting, and we know the type of these elements. This sounds like a job for freezing the type with a const assertion:
exportconstuseToggle=(initialValue:boolean)=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);// here, we freeze the array to a tuplereturn[value,toggleValue]asconst;}
exportconstuseToggle=(initialValue:boolean)=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);// here, we freeze the array to a tuplereturn[value,toggleValue]asconst;}
返回类型现在是readonly [boolean, () => void],因为as const可以确保您的值是恒定的并且不可更改。此类型在语义上略有不同,但实际上您无法更改在 之外返回的值useToggle。因此 beingreadonly会稍微正确一些。
The return type is now readonly [boolean, () => void], because as const makes sure that your values are constant and not changeable. This type is a little bit different semantically, but in reality you wouldn’t be able to change the values you return outside of useToggle. So being readonly would be slightly more correct.
这个问题有几种解决方案。
There are several solutions to this problem.
如果您正在 React 中创建组件库和设计系统,那么您可能已经将 转发ref到组件内的 DOM 元素。
If you are creating component libraries and design systems in React, you might already have fowarded refs to the DOM elements inside your components.
如果你包装基本组件或使用代理组件(参见范例 10.1ref ),但又想像以前一样使用该属性,那么这种方法就特别有用:
This is especially useful if you wrap basic components or leaves in proxy components (see Recipe 10.1), but want to use the ref property just like you’re used to:
constButton=React.forwardRef((props,ref)=>(<buttontype="button"{...props}ref={ref}>{props.children}</button>));// Usage: You can use your proxy just like you use// a regular button!constreference=React.createRef();<ButtonclassName="primary"ref={reference}>Hello</Button>
constButton=React.forwardRef((props,ref)=>(<buttontype="button"{...props}ref={ref}>{props.children}</button>));// Usage: You can use your proxy just like you use// a regular button!constreference=React.createRef();<ButtonclassName="primary"ref={reference}>Hello</Button>
提供 的类型React.forwardRef通常非常简单。 提供的类型@types/react具有通用类型变量,您可以在调用 时设置这些变量React.forwardRef。在这种情况下,明确注释您的类型是正确的做法:
Providing types for React.forwardRef is usually pretty straightforward. The types shipped by @types/react have generic type variables that you can set upon calling React.forwardRef. In that case, explicitly annotating your types is the way to go:
typeButtonProps=JSX.IntrinsicElements["button"];constButton=React.forwardRef<HTMLButtonElement,ButtonProps>((props,ref)=>(<buttontype="button"{...props}ref={ref}>{props.children}</button>));// Usageconstreference=React.createRef<HTMLButtonElement>();<ButtonclassName="primary"ref={reference}>Hello</Button>
typeButtonProps=JSX.IntrinsicElements["button"];constButton=React.forwardRef<HTMLButtonElement,ButtonProps>((props,ref)=>(<buttontype="button"{...props}ref={ref}>{props.children}</button>));// Usageconstreference=React.createRef<HTMLButtonElement>();<ButtonclassName="primary"ref={reference}>Hello</Button>
到目前为止一切顺利。但是,如果您有一个接受通用属性的组件,事情就会变得有点棘手。以下组件生成一个列表项列表,您可以在其中选择带有button元素的每一行:
So far, so good. But things get a bit hairy if you have a component that accepts generic properties. The following component produces a list of list items, where you can select each row with a button element:
typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;};functionClickableList<T>(props:ClickableListProps<T>){return(<ul>{props.items.map((item,idx)=>(<li><buttonkey={idx}onClick={()=>props.onSelect(item)}>Choose</button>{item}</li>))}</ul>);}// Usageconstitems=[1,2,3,4];<ClickableListitems={items}onSelect={(item)=>{// item is of type numberconsole.log(item);}}/>
typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;};functionClickableList<T>(props:ClickableListProps<T>){return(<ul>{props.items.map((item,idx)=>(<li><buttonkey={idx}onClick={()=>props.onSelect(item)}>Choose</button>{item}</li>))}</ul>);}// Usageconstitems=[1,2,3,4];<ClickableListitems={items}onSelect={(item)=>{// item is of type numberconsole.log(item);}}/>
item您需要额外的类型安全性,以便您可以在回调中使用类型安全onSelect。假设您想要创建一个ref内部ul元素:您该怎么做?让我们将ClickableList组件更改为一个内部函数组件,该组件接受一个ForwardRef并将其用作函数中的参数React.forwardRef:
You want the extra type safety so you can work with a type-safe item in your onSelect callback. Say you want to create a ref to the inner ul element: how do you proceed? Let’s change the ClickableList component to an inner function component that takes a ForwardRef and use it as an argument in the React.forwardRef function:
// The original component extended with a `ref`functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}// As an argument in `React.forwardRef`constClickableList=React.forwardRef(ClickableListInner)
// The original component extended with a `ref`functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}// As an argument in `React.forwardRef`constClickableList=React.forwardRef(ClickableListInner)
这可以编译,但有一个缺点:我们不能为 分配一个泛型类型变量。默认情况下,ClickableListProps它变为。与 相比,这很好,但也有点烦人。当我们使用 时,我们知道要传递哪些项目,并且我们希望相应地输入它们!那么我们如何才能做到这一点?答案很棘手……并且您有几个选择。unknownanyClickableList
This compiles but has one downside: we can’t assign a generic type variable for ClickableListProps. It becomes unknown by default. This is good compared to any but also slightly annoying. When we use ClickableList, we know which items to pass along, and we want to have them typed accordingly! So how can we achieve this? The answer is tricky … and you have a couple of options.
第一个选项是进行类型断言以恢复原始函数签名:
The first option is to do a type assertion that restores the original function signature:
constClickableList=React.forwardRef(ClickableListInner)as<T>(props:ClickableListProps<T>&{ref?:React.ForwardedRef<HTMLUListElement>})=>ReturnType<typeofClickableListInner>;
constClickableList=React.forwardRef(ClickableListInner)as<T>(props:ClickableListProps<T>&{ref?:React.ForwardedRef<HTMLUListElement>})=>ReturnType<typeofClickableListInner>;
如果您碰巧只有少数情况需要通用forwardRef组件,类型断言会非常有用,但当您使用大量组件时,它们可能太笨拙了。此外,您为本应是默认行为的某些操作引入了不安全运算符。
Type assertions work great if you happen to have only a few situations where you need generic forwardRef components, but they might be too clumsy when you work with lots of them. Also, you introduce an unsafe operator for something that should be default behavior.
第二种选择是使用包装器组件创建自定义引用。虽然ref是 React 组件的保留字,但您可以使用自己的自定义 props 来模仿类似的行为。这同样有效:
The second option is to create custom references with wrapper components. While ref is a reserved word for React components, you can use your own custom props to mimic a similar behavior. This works just as well:
typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;mRef?:React.Ref<HTMLUListElement>|null;};exportfunctionClickableList<T>(props:ClickableListProps<T>){return(<ulref={props.mRef}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}
typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;mRef?:React.Ref<HTMLUListElement>|null;};exportfunctionClickableList<T>(props:ClickableListProps<T>){return(<ulref={props.mRef}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}
但是,您引入了一个新的 API。顺便说一下,还有一种可能性是使用包装器组件,它允许您forwardRef在内部组件中使用并将自定义属性暴露ref给外部:
You introduce a new API, however. For the record, there is also the possibility of using a wrapper component that allows you to use forwardRef inside an inner component and expose a custom ref property to the outside:
functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}constClickableListWithRef=forwardRef(ClickableListInner);typeClickableListWithRefProps<T>=ClickableListProps<T>&{mRef?:React.Ref<HTMLUListElement>;};exportfunctionClickableList<T>({mRef,...props}:ClickableListWithRefProps<T>){return<ClickableListWithRefref={mRef}{...props}/>;}
functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}constClickableListWithRef=forwardRef(ClickableListInner);typeClickableListWithRefProps<T>=ClickableListProps<T>&{mRef?:React.Ref<HTMLUListElement>;};exportfunctionClickableList<T>({mRef,...props}:ClickableListWithRefProps<T>){return<ClickableListWithRefref={mRef}{...props}/>;}
如果您唯一想要实现的是传递该引用,那么这两种方法都是有效的解决方案。如果您想要拥有一致的 API,您可能需要寻找其他方法。
Both are valid solutions if the only thing you want to achieve is passing that ref. If you want to have a consistent API, you might look for something else.
第三个也是最后一个选项是forwardRef使用您自己的类型定义进行扩充。TypeScript 具有一项称为高阶函数类型推断的功能,允许将自由类型参数传播到外部函数。
The third and final option is to augment forwardRef with your own type definitions. TypeScript has a feature called higher-order function type inference that allows propagating free type parameters to the outer function.
这听起来很像我们forwardRef一开始想要的,但它不适用于我们当前的类型。原因是高阶函数类型推断仅适用于普通函数类型。里面的函数声明
forwardRef还添加了属性defaultProps等。这些都是类组件时代的遗留物,无论如何你可能都不想使用它们。
This sounds a lot like what we want with forwardRef to begin with, but it doesn’t work with our current typings. The reason is that higher-order function type inference works only on plain function types. The function declarations inside
forwardRef also add properties for defaultProps and so on. These are relics from the class component days, things you might not want to use anyway.
因此,即使没有附加属性,也应该可以使用高阶 函数类型推断!
So without the additional properties, it should be possible to use higher-order function type inference!
我们正在使用 TypeScript,因此我们可以自行重新声明和重新定义全局
module、namespace和interface声明。声明合并是一个强大的工具,我们将使用它:
We are using TypeScript, so we have the ability to redeclare and redefine global
module, namespace, and interface declarations on our own. Declaration merging is a powerful tool, and we’re going to use it:
// Redecalare forwardRefdeclaremodule"react"{functionforwardRef<T,P={}>(render:(props:P,ref:React.Ref<T>)=>React.ReactElement|null):(props:P&React.RefAttributes<T>)=>React.ReactElement|null;}// Just write your components like you're used to!typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;};functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}exportconstClickableList=React.forwardRef(ClickableListInner);
// Redecalare forwardRefdeclaremodule"react"{functionforwardRef<T,P={}>(render:(props:P,ref:React.Ref<T>)=>React.ReactElement|null):(props:P&React.RefAttributes<T>)=>React.ReactElement|null;}// Just write your components like you're used to!typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;};functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}exportconstClickableList=React.forwardRef(ClickableListInner);
此解决方案的优点在于,您可以再次编写常规 JavaScript,并专门在类型级别上工作。此外,重新声明是模块范围的:不会干扰forwardRef来自其他模块的任何调用!
The nice thing about this solution is that you write regular JavaScript again and work exclusively on a type level. Also, redeclarations are module scoped: no interference with any forwardRef calls from other modules!
要么为上下文设置默认属性并让其类型被推断出来,要么创建上下文属性的一部分并显式实例化泛型类型参数。如果您不想提供默认值,但想确保提供所有属性,请创建一个辅助函数。
Either set default properties for context and let the type be inferred or create a partial of your context’s properties and instantiate the generic type parameter explicitly. If you don’t want to provide default values, but want to make sure that all properties are provided, create a helper function.
React 的 context API 允许你在全局层面共享数据。要使用它,你需要两样东西:
React’s context API allows you to share data on a global level. To use it, you need two things:
提供者将数据传递至子树。
Providers pass data to a subtree.
消费者是使用渲染道具内传递的数据的组件。
Consumers are components that consume the passed data inside render props.
借助 React 的类型,大多数时候你可以使用上下文而无需执行任何其他操作。一切都通过类型推断和泛型完成。
With React’s typings, you can use context without doing anything else most of the time. Everything is done using type inference and generics.
首先,我们创建一个上下文。在这里,我们希望存储全局应用程序设置(如主题和应用程序的语言)以及全局状态。创建 React 上下文时,我们希望传递默认属性:
First, we create a context. Here, we want to store global application settings, like a theme and the app’s language, along with the global state. When creating a React context, we want to pass default properties:
importReactfrom"react";constAppContext=React.createContext({authenticated:true,lang:"en",theme:"dark",});
importReactfrom"react";constAppContext=React.createContext({authenticated:true,lang:"en",theme:"dark",});
这样,您需要做的所有类型相关的事情都已为您完成。我们有三个属性:authenticated、lang和theme;它们的类型boolean为和string。React 的类型会利用这些信息,在您使用它们时为您提供正确的类型。
And with that, everything you need to do in terms of types is done for you. We have three properties: authenticated, lang, and theme; they are of types boolean and string. React’s typings
take this information to provide you with the correct types when you use them.
接下来,组件树中较高级别的组件需要提供上下文,例如应用程序的根组件。此提供程序会将您设置的值传递给下面的每个消费者:
Next, a component high up in your component tree needs to provide context—for example, the application’s root component. This provider trickles down the values you’ve set to every consumer below:
functionApp(){return(<AppContext.Providervalue={{authenticated:true,lang:"de",theme:"light",}}><Header/></AppContext.Provider>);}
functionApp(){return(<AppContext.Providervalue={{authenticated:true,lang:"de",theme:"light",}}><Header/></AppContext.Provider>);}
现在,此树中的每个组件都可以使用此上下文。当您忘记属性或使用错误的类型时,您已经收到类型错误:
Now, every component inside this tree can consume this context. You already get type errors when you forget a property or use the wrong type:
functionApp(){// Property 'theme' is missing in type '{ lang: string; }' but required// in type '{ lang: string; theme: string; authenticated: boolean }'.(2741)return(<AppContext.Providervalue={{lang:"de",}}><Header/></AppContext.Provider>);}
functionApp(){// Property 'theme' is missing in type '{ lang: string; }' but required// in type '{ lang: string; theme: string; authenticated: boolean }'.(2741)return(<AppContext.Providervalue={{lang:"de",}}><Header/></AppContext.Provider>);}
现在,让我们使用我们的全局状态。使用上下文可以通过渲染道具来完成。您可以根据需要对渲染道具进行深度解构,以仅获取您想要处理的道具:
Now, let’s consume our global state. Consuming context can be done via render props. You can destructure your render props as deep as you like, to get only the props you want to deal with:
functionHeader(){return(<AppContext.Consumer>{({authenticated})=>{if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}}</AppContext.Consumer>);}
functionHeader(){return(<AppContext.Consumer>{({authenticated})=>{if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}}</AppContext.Consumer>);}
使用上下文的另一种方法是通过相应的useContext钩子:
Another way of using context is via the respective useContext hook:
functionHeader(){const{authenticated}=useContext(AppContext);if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}
functionHeader(){const{authenticated}=useContext(AppContext);if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}
因为我们之前用正确的类型定义了属性,authenticated所以此时是布尔类型。同样,我们不需要做任何事情来获得这种额外的
类型安全性。
Because we defined our properties earlier with the right types, authenticated is of type boolean at this point. Again,
we didn’t have to do anything to get this extra
type safety.
如果我们有默认属性和值,整个上例效果会最好。有时您没有默认值,或者您需要更灵活地设置属性。
The whole previous example works best if we have default properties and values. Sometimes you don’t have default values or you need to be more flexible in which properties you want to set.
我们不是根据默认值推断所有内容,而是使用 来明确注释泛型类型参数,不是使用完整类型,而是使用Partial。
Instead of inferring everything from default values, we annotate the generic type parameter explicitly, not with the full type, but with a Partial.
我们为上下文的 props 创建一个类型:
We create a type for the context’s props:
typeContextProps={authenticated:boolean;lang:string;theme:string;};
typeContextProps={authenticated:boolean;lang:string;theme:string;};
并初始化新的上下文:
And initialize the new context:
constAppContext=React.createContext<Partial<ContextProps>>({});
constAppContext=React.createContext<Partial<ContextProps>>({});
更改上下文默认属性的语义也会对您的组件产生一些副作用。现在您不需要提供每个值;一个空的上下文对象就可以完成相同的操作!您的所有属性都是可选的:
Changing the semantics of the context’s default properties has some side effects on your components as well. Now you don’t need to provide every value; an empty context object can do the same! All your properties are optional:
functionApp(){return(<AppContext.Providervalue={{authenticated:true,}}><Header/></AppContext.Provider>);}
functionApp(){return(<AppContext.Providervalue={{authenticated:true,}}><Header/></AppContext.Provider>);}
这也意味着您需要检查每个属性是否已定义。这不会更改您依赖boolean值的代码,但每个其他属性都需要进行另一次undefined检查:
This also means you need to check for every property if it’s defined. This doesn’t change the code where you rely on boolean values, but every other property needs to have another undefined check:
functionHeader(){const{authenticated,lang}=useContext(AppContext);if(authenticated&&lang){return<><h1>Loggedin!</h1><p>Yourlanguagesettingissetto{lang}</p></>;}return<h1>Youneedtosignin(ordon'tyouhavealanguagesetting?)</h1>;}
functionHeader(){const{authenticated,lang}=useContext(AppContext);if(authenticated&&lang){return<><h1>Loggedin!</h1><p>Yourlanguagesettingissetto{lang}</p></>;}return<h1>Youneedtosignin(ordon'tyouhavealanguagesetting?)</h1>;}
如果您无法提供默认值,并且想要确保所有属性都由上下文提供程序提供,则可以使用辅助函数来帮助自己。在这里,我们希望显式泛型实例化提供类型,但提供正确的类型保护,以便在使用上下文时,所有可能未定义的值都得到正确设置:
If you can’t provide default values and want to make sure that all properties are provided by a context provider, you can help yourself with a helper function. Here, we want explicit generic instantiation to supply a type but give the right type guards so that when consuming context, all possibly undefined values are correctly set:
functioncreateContext<Propsextends{}>(){constctx=React.createContext<Props|undefined>(undefined);functionuseInnerCtx(){constc=useContext(ctx);if(c===undefined)thrownewError("Context must be consumed within a Provider");returnc;}return[useInnerCtx,ctx.ProviderasReact.Provider<Props>]asconst;}
functioncreateContext<Propsextends{}>(){constctx=React.createContext<Props|undefined>(undefined);functionuseInnerCtx(){constc=useContext(ctx);if(c===undefined)thrownewError("Context must be consumed within a Provider");returnc;}return[useInnerCtx,ctx.ProviderasReact.Provider<Props>]asconst;}
发生什么事了createContext?
What’s going on in createContext?
我们创建一个没有函数参数但有泛型类型参数的函数。如果没有与函数参数的连接,我们就无法Props通过推断进行实例化。这意味着为了createContext提供正确的类型,我们需要显式实例化它。
We create a function that has no function arguments but generic type parameters. Without the connection to function parameters, we can’t instantiate Props via inference. This means that for createContext to provide proper types, we need to explicitly instantiate it.
我们创建一个允许 forProps或 的上下文undefined。undefined添加到类型后,我们可以将其undefined作为值传递。没有默认值!
We create a context that allows for Props or undefined. With undefined added to the type, we can pass undefined as value. No default values!
在里面createContext,我们创建一个自定义钩子。此钩子useContext使用新创建的上下文进行包装ctx。
Inside createContext, we create a custom hook. This hook wraps useContext using the newly created context ctx.
然后我们做一个类型保护,检查返回的是否Props包含
undefined。记住,当调用时createContext,我们用实例化泛型类型参数Props | undefined。此行undefined再次从联合类型中删除。
Then we do a type guard where we check if the returned Props includes
undefined. Remember, when calling createContext, we instantiate the generic type parameter with Props | undefined. This line removes undefined from the union type again.
这意味着这里的c是Props。
Which means that here, c is Props.
我们断言ctx.Provider不接受undefined值。我们调用以作为元组类型as const返回。[useInnerContext, ctx.Provider]
We assert that ctx.Provider doesn’t take undefined values. We call as const to return [useInnerContext, ctx.Provider] as a tuple type.
使用createContext类似于React.createContext:
Use createContext similar to React.createContext:
const[useAppContext,AppContextProvider]=createContext<ContextProps>();
const[useAppContext,AppContextProvider]=createContext<ContextProps>();
使用时AppContextProvider,我们需要提供所有值:
When using AppContextProvider, we need to provide all values:
functionApp(){return(<AppContextProvidervalue={{lang:"en",theme:"dark",authenticated:true}}><Header/></AppContextProvider>);}functionHeader(){// consuming Context doesn't change muchconst{authenticated}=useAppContext();if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}
functionApp(){return(<AppContextProvidervalue={{lang:"en",theme:"dark",authenticated:true}}><Header/></AppContextProvider>);}functionHeader(){// consuming Context doesn't change muchconst{authenticated}=useAppContext();if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}
Depending on your use case, you have exact types without too much overhead.
使用React.ComponentType<P>类型@types/react来定义扩展预设属性的组件。
Use the React.ComponentType<P> type from @types/react to define a component that extends your preset attributes.
React 受到函数式编程的影响,我们在组件的设计方式(通过函数)、组装方式(通过组合)和更新方式(无状态、单向数据流)中都可以看到这一点。函数式编程技术和范式很快就进入了 React 开发。高阶组件就是这样一种技术,它的灵感来自高阶函数。
React is influenced by functional programming, which we see in the way components are designed (via functions), assembled (via composition), and updated (stateless, unidirectional data flow). It didn’t take long for functional programming techniques and paradigms to find their way into React development. One such technique is higher-order components, which draw inspiration from higher-order functions.
高阶函数接受一个或多个参数来返回一个新函数。有时这些参数在这里是为了预填充某些其他参数,例如,我们在第 7 章的所有柯里化配方中看到的那样。高阶组件类似:它们接受一个或多个组件并返回另一个组件。通常,您创建它们是为了预填充某些属性,以确保它们以后不会被更改。
Higher-order functions accept one or more parameters to return a new function. Sometimes those parameters are here to prefill certain other parameters, as we see, for example, in all currying recipes from Chapter 7. Higher-order components are similar: they take one or more components and return themselves another component. Usually, you create them to prefill certain properties where you want to make sure they won’t be changed later on.
考虑一个通用Card组件,它将title和content
作为字符串:
Think about a general-purpose Card component, which takes title and content
as strings:
typeCardProps={title:string;content:string;};functionCard({title,content}:CardProps){return(<><h2>{title}</h2><div>{content}</div></>);}
typeCardProps={title:string;content:string;};functionCard({title,content}:CardProps){return(<><h2>{title}</h2><div>{content}</div></>);}
您可以使用此卡片来显示某些事件,例如警告、信息气泡和错误消息。最基本的信息卡的"Info"标题为:
You use this card to present certain events, like warnings, information bubbles, and error messages. The most basic information card has "Info" as its title:
<Cardtitle="Info"content="Your task has been processed"/>;
<Cardtitle="Info"content="Your task has been processed"/>;
您可以对 的属性进行子集化,Card以仅允许 的某个字符串子集title,但另一方面,您希望能够Card尽可能多地重用 。因此,您创建一个新组件,该组件已设置title为"Info",并且仅允许设置其他属性:
You could subset the properties of Card to allow for only a certain subset of strings for title, but on the other hand, you want to be able to reuse Card as much as possible. So you create a new component that already sets title to "Info" and only allows for other properties to be set:
constInfo=withInjectedProps({title:"Info"},Card);// This should work<Infocontent="Your task has been processed"/>;// This should throw an error<Infocontent="Your task has been processed"title="Warning"/>;
constInfo=withInjectedProps({title:"Info"},Card);// This should work<Infocontent="Your task has been processed"/>;// This should throw an error<Infocontent="Your task has been processed"title="Warning"/>;
换句话说,您可以注入属性的子集,并使用新创建的组件设置其余属性。函数withInjectedProps很容易编写:
In other words, you inject a subset of properties and set the remaining ones with the newly created component. A function withInjectedProps is easily written:
functionwithInjectedProps(injected,Component){returnfunction(props){constnewProps={...injected,...props};return<Component{...newProps}/>;};}
functionwithInjectedProps(injected,Component){returnfunction(props){constnewProps={...injected,...props};return<Component{...newProps}/>;};}
它将injectedprops 和 aComponent作为参数,返回一个将剩余 props 作为参数的新函数组件,并使用合并的属性实例化原始组件。
It takes the injected props and a Component as parameters, returns a new function component that takes the remaining props as parameters, and instantiates the original component with merged properties.
那么我们如何输入呢withInjectedProps?让我们看看结果,看看里面有什么:
So how do we type withInjectedProps? Let’s look at the result and see what’s inside:
functionwithInjectedProps<Textends{},UextendsT>(injected:T,Component:React.ComponentType<U>){returnfunction(props:Omit<U,keyofT>){constnewProps={...injected,...props}asU;return<Component{...newProps}/>;};}
functionwithInjectedProps<Textends{},UextendsT>(injected:T,Component:React.ComponentType<U>){returnfunction(props:Omit<U,keyofT>){constnewProps={...injected,...props}asU;return<Component{...newProps}/>;};}
事情是这样的:
Here is what’s going on:
我们需要定义两个泛型类型参数。T是针对我们已经注入的 props;它从 扩展而来,{}以确保我们只传递对象。U是针对 的所有 props 的泛型类型参数Component。U 扩展 T,这意味着U是 的子集T。 这表示U具有比 更多的属性,T但需要包含T已经定义的属性。
We need to define two generic type parameters. T is for the props we already inject; it extends from {} to make sure we only pass objects. U is a generic type parameter for all props of Component. U extends T, which means that U is a subset of T. This says that U has more properties than T but needs to include what T already has defined.
我们定义Component为 类型React.ComponentType<U>。这包括类组件和函数组件,并表示 props 将设置为。通过和U的关系以及我们定义 参数的方式,我们确保传递给 的所有内容都定义了属性的子集。如果我们输入错误,我们会很快收到第一条错误消息!TUwithInjectedPropsComponentComponentinjected
We define Component to be of type React.ComponentType<U>. This includes class components as well as function components and says that props will be set to U. With the relationship of T and U and the way we defined the parameters of withInjectedProps, we ensure that everything that will be passed for Component defines a subset of properties for Component with injected. If we make a typo, we quickly get the first error message!
将返回的函数组件将接受剩余的 props。Omit<U, keyof T>我们确保不允许再次设置预填充的属性。
The function component that will be returned takes the remaining props. With Omit<U, keyof T> we make sure that we don’t allow prefilled attributes to be set again.
合并T和Omit<U, keyof T>应该会再次产生U,但由于泛型类型参数可以用其他类型显式实例化,因此它们可能不再适合Component。类型断言有助于确保 props 确实是我们想要的。
Merging T and Omit<U, keyof T> should result in U again, but since generic type parameters can be explicitly instantiated with something different, they might not fit Component again. A type assertion helps ensure that the props are actually what we want.
就是这样!有了这些新类型,我们就可以得到正确的自动完成和错误:
And that’s it! With those new types, we get proper autocomplete and errors:
constInfo=withInjectedProps({title:"Info"},Card);<Infocontent="Your task has been processed"/>;<Infocontent="Your task has been processed"title="Warning"/>;// ^// Type '{ content: string; title: string; }' is not assignable// to type 'IntrinsicAttributes & Omit<CardProps, "title">'.// Property 'title' does not exist on type// 'IntrinsicAttributes & Omit<CardProps, "title">'.(2322)
constInfo=withInjectedProps({title:"Info"},Card);<Infocontent="Your task has been processed"/>;<Infocontent="Your task has been processed"title="Warning"/>;// ^// Type '{ content: string; title: string; }' is not assignable// to type 'IntrinsicAttributes & Omit<CardProps, "title">'.// Property 'title' does not exist on type// 'IntrinsicAttributes & Omit<CardProps, "title">'.(2322)
withInjectedProps非常灵活,我们可以派生出高阶函数,为各种情况创建高阶组件,例如withTitle,它在这里预填充title类型的属性string:
withInjectedProps is so flexible that we can derive higher-order functions that create higher-order components for various situations, like withTitle, which is here to prefill title attributes of type string:
functionwithTitle<Uextends{title:string}>(title:string,Component:React.ComponentType<U>){returnwithInjectedProps({title},Component);}
functionwithTitle<Uextends{title:string}>(title:string,Component:React.ComponentType<U>){returnwithInjectedProps({title},Component);}
使用事件类型@types/react并使用泛型类型参数专门针对组件。
Use the event types of @types/react and specialize on components using generic type parameters.
Web 应用程序通过用户交互变得活跃。每次用户交互都会触发一个事件。事件是关键,TypeScript 的 React 类型对事件有很好的支持,但它们要求您不要使用lib.dom.d.ts中的本机事件。如果您这样做,React 会抛出错误:
Web applications become alive through user interaction. Every user interaction triggers an event. Events are key, and TypeScript’s React typings have great support for events, but they require you not to use the native events from lib.dom.d.ts. If you do, React throws errors:
typeWithChildren<T={}>=T&{children?:React.ReactNode};typeButtonProps={onClick:(event:MouseEvent)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;// ^// Type '(event: MouseEvent) => void' is not assignable to// type 'MouseEventHandler<HTMLButtonElement>'.// Types of parameters 'event' and 'event' are incompatible.// Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is missing the following// properties from type 'MouseEvent': offsetX, offsetY, x, y,// and 14 more.(2322)}
typeWithChildren<T={}>=T&{children?:React.ReactNode};typeButtonProps={onClick:(event:MouseEvent)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;// ^// Type '(event: MouseEvent) => void' is not assignable to// type 'MouseEventHandler<HTMLButtonElement>'.// Types of parameters 'event' and 'event' are incompatible.// Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is missing the following// properties from type 'MouseEvent': offsetX, offsetY, x, y,// and 14 more.(2322)}
React 使用自己的事件系统,我们将其称为合成事件。合成事件是浏览器原生事件的跨浏览器包装器,具有与原生事件相同的接口,但为了兼容性而保持一致。更改类型@types/react可使您的回调再次兼容:
React uses its own event system, which we refer to as synthetic events. Synthetic events are cross-browser wrappers around the browser’s native event, with the same interface as its native counterpart but aligned for compatibility. A change to the type from @types/react makes your callbacks compatible again:
importReactfrom"react";typeWithChildren<T={}>=T&{children?:React.ReactNode};typeButtonProps={onClick:(event:React.MouseEvent)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;}
importReactfrom"react";typeWithChildren<T={}>=T&{children?:React.ReactNode};typeButtonProps={onClick:(event:React.MouseEvent)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;}
对于 TypeScript 的结构类型系统来说,浏览器的MouseEvent和React.MouseEvent有足够的不同,这意味着合成对应物中缺少一些属性。您可以在前面的错误消息中看到,原始属性比 多了 18 个属性,其中一些属性可能很重要,例如坐标和偏移量,如果您想在画布上绘图,这些属性会派上用场。MouseEventReact.MouseEvent
The browser’s MouseEvent and React.MouseEvent are different enough for TypeScript’s structural type system, meaning that there are some missing properties in the synthetic counterparts. You can see in the preceding error message that the original MouseEvent has 18 properties more than React.MouseEvent, some of them arguably important, like coordinates and offsets, which come in handy if, for example, you want to draw on a canvas.
如果要访问原始事件的属性,可以使用以下nativeEvent属性:
If you want to access properties from the original event, you can use the nativeEvent property:
functionhandleClick(event:React.MouseEvent){console.log(event.nativeEvent.offsetX,event.nativeEvent.offsetY);}constbtn=<ButtononClick={handleClick}>Hello</Button>};
functionhandleClick(event:React.MouseEvent){console.log(event.nativeEvent.offsetX,event.nativeEvent.offsetY);}constbtn=<ButtononClick={handleClick}>Hello</Button>};
支持的事件包括:AnimationEvent、、、、、、、、、、、、和,以及所有其他事件。ChangeEventClipboardEventCompositionEventDragEventFocusEventFormEventKeyboardEventMouseEventPointerEventTouchEventTransitionEventWheelEventSyntheticEvent
Events supported are: AnimationEvent, ChangeEvent, ClipboardEvent, CompositionEvent, DragEvent, FocusEvent, FormEvent, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, TransitionEvent, and WheelEvent, as well as SyntheticEvent for all other events.
到目前为止,我们应用了正确的类型以确保没有任何编译器错误。很简单。但是我们使用 TypeScript 不仅是为了完成应用类型的仪式,以防止编译器抱怨,也是为了防止可能出现问题的情况。
So far, we applied the correct types to make sure we don’t have any compiler errors. Easy enough. But we’re using TypeScript not only to fulfill the ceremony of applying types to keep the compiler from complaining but also to prevent situations that might be problematic.
让我们再想想按钮。或者链接(元素a)。这些元素应该被点击;这就是它们的目的。但在浏览器中,每个元素都可以接收点击事件。没有什么可以阻止你向元素添加onClick,div元素是所有元素中语义含义最少的元素,并且没有辅助技术会告诉你,除非你向元素添加大量属性,否则元素div可以接收MouseEvent。
Let’s think about a button again. Or a link (the a element). Those elements are supposed to be clicked; that’s their purpose. But in the browser, click events can be received by every element. Nothing keeps you from adding onClick to a div element, the element that has the least semantic meaning of all elements, and no assistive technology will tell you that a div can receive a MouseEvent unless you add lots of attributes to it.
如果我们可以防止同事(和我们自己)在错误的元素上使用已定义的事件处理程序,那不是很好吗?React.MouseEvent是一种通用类型,它将兼容元素作为其第一种类型。 它设置为Element,这是浏览器中所有元素的基本类型。 但是,您可以通过子类型化此通用参数来定义一组较小的兼容元素:
Wouldn’t it be great if we could keep our colleagues (and ourselves) from using the defined event handlers on the wrong elements? React.MouseEvent is a generic type that takes compatible elements as its first type. This is set to Element, which is the base type for all elements in the browser. But you are able to define a smaller set of compatible elements by subtyping this generic parameter:
typeWithChildren<T={}>=T&{children?:React.ReactNode};// Button maps to an HTMLButtonElementtypeButtonProps={onClick:(event:React.MouseEvent<HTMLButtonElement>)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;}// handleClick accepts events from HTMLButtonElement or HTMLAnchorElementfunctionhandleClick(event:React.MouseEvent<HTMLButtonElement|HTMLAnchorElement>){console.log(event.currentTarget.tagName);}letbutton=<ButtononClick={handleClick}>Works</Button>;letlink=<ahref="/"onClick={handleClick}>Works</a>;letbroken=<divonClick={handleClick}>Doesnotwork</div>;// ^// Type '(event: MouseEvent<HTMLButtonElement | HTMLAnchorElement,// MouseEvent>) => void' is not assignable to type//'MouseEventHandler<HTMLDivElement>'.// Types of parameters 'event' and 'event' are incompatible.// Type 'MouseEvent<HTMLDivElement, MouseEvent>' is not assignable to// type 'MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>'.// Type 'HTMLDivElement' is not assignable to type #// 'HTMLButtonElement | HTMLAnchorElement'.
typeWithChildren<T={}>=T&{children?:React.ReactNode};// Button maps to an HTMLButtonElementtypeButtonProps={onClick:(event:React.MouseEvent<HTMLButtonElement>)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;}// handleClick accepts events from HTMLButtonElement or HTMLAnchorElementfunctionhandleClick(event:React.MouseEvent<HTMLButtonElement|HTMLAnchorElement>){console.log(event.currentTarget.tagName);}letbutton=<ButtononClick={handleClick}>Works</Button>;letlink=<ahref="/"onClick={handleClick}>Works</a>;letbroken=<divonClick={handleClick}>Doesnotwork</div>;// ^// Type '(event: MouseEvent<HTMLButtonElement | HTMLAnchorElement,// MouseEvent>) => void' is not assignable to type//'MouseEventHandler<HTMLDivElement>'.// Types of parameters 'event' and 'event' are incompatible.// Type 'MouseEvent<HTMLDivElement, MouseEvent>' is not assignable to// type 'MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>'.// Type 'HTMLDivElement' is not assignable to type #// 'HTMLButtonElement | HTMLAnchorElement'.
尽管 React 的类型在某些方面为您提供了更多灵活性,但它在其他方面缺乏功能。例如,浏览器原生InputEvent不支持@types/react。合成事件系统旨在成为跨浏览器解决方案,而 React 的一些兼容浏览器仍然缺乏 的实现InputEvent。在它们赶上之前,您可以安全地使用基本事件SyntheticEvent:
Although React’s types give you more flexibility in some areas, it lacks features in others. For example, the browser native InputEvent is not supported in @types/react. The synthetic event system is meant to be a cross-browser solution, and some of React’s compatible browsers still lack implementation of InputEvent. Until they catch up, it’s safe for you to use the base event SyntheticEvent:
functiononInput(event:React.SyntheticEvent){event.preventDefault();// do something}constinp=<inputtype="text"onInput={onInput}/>;
functiononInput(event:React.SyntheticEvent){event.preventDefault();// do something}constinp=<inputtype="text"onInput={onInput}/>;
你创建了一个代理组件(参见范例 10.1),该组件需要充当许多不同 HTML 元素之一。很难获得正确的类型。
You create a proxy component (see Recipe 10.1) that needs to behave as one of many different HTML elements. It’s hard to get the right typings.
断言转发的属性any或直接使用 JSX 工厂React.createElement。
Assert forwarded properties as any or use the JSX factory React.createElement directly.
React 中的一种常见模式是定义多态(或as)组件,这些组件预定义行为但可以充当不同的元素。想想行动号召按钮或 CTA,它可以是指向网站的链接或实际的 HTML 按钮。如果您想为它们设置类似的样式,它们的行为应该相同,但根据上下文,它们应该具有正确的 HTML 元素以执行正确的操作。
A common pattern in React is to define polymorphic (or as) components, which pre-define behavior but can act as different elements. Think of a call-to-action button, or CTA, which can be a link to a website or an actual HTML button. If you want to style them similarly, they should behave alike, but depending on the context they should have the right HTML element for the right action.
选择正确的元素是重要的可访问性因素。a和button元素代表用户可以点击的内容,但 的语义a与 的语义根本不同button。是锚点的缩写,需要对目标a有引用 ( )。可以点击,但操作通常通过 JavaScript 编写脚本。 这两个元素可能看起来相同,但它们的行为不同。 它们不仅行为不同,而且使用辅助技术(如屏幕阅读器)的显示方式也不同。 考虑您的用户,并为正确的目的选择正确的元素。hrefbutton
Selecting the right element is an important accessibility factor. a and button elements represent something users can click, but the semantics of a are fundamentally different from the semantics of a button. a is short for anchor and needs to have a reference (href) to a destination. A button can be clicked, but the action is usually scripted via JavaScript. Both elements can look the same, but they act differently. Not only do they act differently, but they also are announced differently using assistive technologies, like screen readers. Think about your users and select the right element for the right purpose.
as这个想法是,组件中有一个用于选择元素类型的 prop。根据 的元素类型as,您可以转发适合该元素类型的属性。当然,您可以将此模式与您在方案 10.1中看到的所有内容结合起来:
The idea is that you have an as prop in your component that selects the element type. Depending on the element type of as, you can forward properties that fit the element type. Of course, you can combine this pattern with everything that you have seen in Recipe 10.1:
<Ctaas="a"href="https://typescript-cookbook.com">Heyhey</Cta><Ctaas="button"type="button"onClick={(e)=>{/* do something */}}>Mymy</Cta>
<Ctaas="a"href="https://typescript-cookbook.com">Heyhey</Cta><Ctaas="button"type="button"onClick={(e)=>{/* do something */}}>Mymy</Cta>
当将 TypeScript 投入使用时,您需要确保对正确的 props 进行自动完成,对错误的属性进行错误提示。如果您将一个添加href到 a button,TypeScript 应该会给出正确的波浪线:
When throwing TypeScript into the mix, you want to make sure that you get autocomplete for the right props and errors for the wrong properties. If you add an href to a button, TypeScript should give you the correct squiggly lines:
// Type '{ children: string; as: "button"; type: "button"; href: string; }'// is not assignable to type 'IntrinsicAttributes & { as: "button"; } &// ClassAttributes<HTMLButtonElement> &// ButtonHTMLAttributes<HTMLButtonElement> & { ...; }'.// Property 'href' does not exist on type ... (2322)// v<Ctaas="button"type="button"href=""ref={(el)=>el?.id}>Mymy</Cta>
// Type '{ children: string; as: "button"; type: "button"; href: string; }'// is not assignable to type 'IntrinsicAttributes & { as: "button"; } &// ClassAttributes<HTMLButtonElement> &// ButtonHTMLAttributes<HTMLButtonElement> & { ...; }'.// Property 'href' does not exist on type ... (2322)// v<Ctaas="button"type="button"href=""ref={(el)=>el?.id}>Mymy</Cta>
让我们尝试输入Cta。首先,我们开发完全没有类型的组件。在 JavaScript 中,事情看起来并不太复杂:
Let’s try to type Cta. First, we develop the component without types at all. In JavaScript, things don’t look too complicated:
functionCta({as:Component,...props}){return<Component{...props}/>;}
functionCta({as:Component,...props}){return<Component{...props}/>;}
我们提取asprop 并将其重命名为Component。这是 JavaScript 中的一种解构机制,其语法类似于 TypeScript 注释,但适用于解构属性,而不适用于对象本身(您需要类型注释)。我们将其重命名为大写组件,以便我们可以通过 JSX 实例化它。...props创建组件时,其余的 props 将被收集并分散。请注意,您还可以使用 分散子项...props,这是 JSX 的一个不错的小副作用。
We extract the as prop and rename it as Component. This is a destructuring mechanism from JavaScript that is syntactically similar to a TypeScript annotation but works on destructured properties and not on the object itself (where you’d need a type annotation). We rename it to an uppercase component so we can instantiate it via JSX. The remaining props will be collected in ...props and spread out when creating the component. Note that you can also spread out children with ...props, a nice little side effect of JSX.
当我们想要输入时Cta,我们创建一个适用于元素或元素的CtaProps类型,并从中获取剩余的 props ,类似于我们在配方 10.1中看到的:"a""button"JSX.IntrinsicElements
When we want to type Cta, we create a CtaProps type that works on either "a" elements or "button" elements and takes the remaining props from JSX.IntrinsicElements, similar to what we’ve seen in Recipe 10.1:
typeCtaElements="a"|"button";typeCtaProps<TextendsCtaElements>={as:T;}&JSX.IntrinsicElements[T];
typeCtaElements="a"|"button";typeCtaProps<TextendsCtaElements>={as:T;}&JSX.IntrinsicElements[T];
当我们将类型连接到时Cta,我们看到函数签名只需几个额外的注释就可以很好地工作。但是在实例化组件时,我们得到了一个相当复杂的错误,告诉我们出了多少问题:
When we wire up our types to Cta, we see that the function signature works very well with just a few extra annotations. But when instantiating the component, we get quite an elaborate error that tells us how much is going wrong:
functionCta<TextendsCtaElements>({as:Component,...props}:CtaProps<T>){return<Component{...props}/>;// ^// Type 'Omit<CtaProps<T>, "as" | "children"> & { children: ReactNode; }'// is not assignable to type 'IntrinsicAttributes &// LibraryManagedAttributes<T, ClassAttributes<HTMLAnchorElement> &// AnchorHTMLAttributes<HTMLAnchorElement> & ClassAttributes<...> &// ButtonHTMLAttributes<...>>'.// Type 'Omit<CtaProps<T>, "as" | "children"> & { children: ReactNode; }' is not// assignable to type// 'LibraryManagedAttributes<T, ClassAttributes<HTMLAnchorElement>// & AnchorHTMLAttributes<HTMLAnchorElement> & ClassAttributes<...>// & ButtonHTMLAttributes<...>>'.(2322)}
functionCta<TextendsCtaElements>({as:Component,...props}:CtaProps<T>){return<Component{...props}/>;// ^// Type 'Omit<CtaProps<T>, "as" | "children"> & { children: ReactNode; }'// is not assignable to type 'IntrinsicAttributes &// LibraryManagedAttributes<T, ClassAttributes<HTMLAnchorElement> &// AnchorHTMLAttributes<HTMLAnchorElement> & ClassAttributes<...> &// ButtonHTMLAttributes<...>>'.// Type 'Omit<CtaProps<T>, "as" | "children"> & { children: ReactNode; }' is not// assignable to type// 'LibraryManagedAttributes<T, ClassAttributes<HTMLAnchorElement>// & AnchorHTMLAttributes<HTMLAnchorElement> & ClassAttributes<...>// & ButtonHTMLAttributes<...>>'.(2322)}
那么这个消息从何而来?为了让 TypeScript 正确地与 JSX 协同工作,我们需要借助名为 的全局命名空间中的类型定义JSX。如果此命名空间在范围内,TypeScript 就知道哪些不是组件的元素可以实例化以及它们可以接受哪些属性。这些就是JSX.IntrinsicElements我们在本例和方案 10.1中使用的。
So where does this message come from? For TypeScript to work correctly with JSX, we need to resort to type definitions in a global namespace called JSX. If this namespace is in scope, TypeScript knows which elements that aren’t components can be instantiated and which attributes they can accept. These are the JSX.IntrinsicElements we use in this example and in Recipe 10.1.
还需要定义的一种类型是LibraryManagedAttributes。此类型用于提供由框架本身(如key)或通过以下方式定义的属性defaultProps:
One type that also needs to be defined is LibraryManagedAttributes. This type is used to provide attributes that are defined either by the framework itself (like key) or via means like defaultProps:
exportinterfaceProps{name:string;}functionGreet({name}:Props){return<div>Hello{name.toUpperCase()}!</div>;}// Goes into LibraryManagedAttributesGreet.defaultProps={name:"world"};// Type-checks! No type assertions needed!letel=<Greetkey={1}/>;
exportinterfaceProps{name:string;}functionGreet({name}:Props){return<div>Hello{name.toUpperCase()}!</div>;}// Goes into LibraryManagedAttributesGreet.defaultProps={name:"world"};// Type-checks! No type assertions needed!letel=<Greetkey={1}/>;
React 的类型问题LibraryManagedAttributes通过使用条件类型来解决。正如我们在12.7 节中看到的,条件类型在求值时不会使用联合类型的所有可能变体进行扩展。这意味着 TypeScript 无法检查你的类型是否适合组件,因为它无法求值LibraryManagedAttributes。
React’s typings solve LibraryManagedAttributes by using a conditional type. And as we see in Recipe 12.7, conditional types won’t be expanded with all possible variants of a union type when being evaluated. This means that TypeScript won’t be able to check that your typings fit the components because it won’t be able to evaluate LibraryManagedAttributes.
One workaround for this is to assert props to any:
functionCta<TextendsCtaElements>({as:Component,...props}:CtaProps<T>){return<Component{...(propsasany)}/>;}
functionCta<TextendsCtaElements>({as:Component,...props}:CtaProps<T>){return<Component{...(propsasany)}/>;}
虽然这样可行,但这是本不安全的操作的标志。另一种方法是在这种情况下不使用 JSX,而是使用 JSX 工厂React.createElement。
That works, but it is a sign of an unsafe operation that shouldn’t be unsafe. Another way is to not use JSX in this case but use the JSX factory React.createElement.
每个 JSX 调用都是 JSX 工厂调用的语法糖:
Every JSX call is syntactic sugar to a JSX factory call:
<h1className="headline">HelloWorld</h1>// will be transformed toReact.createElement("h1",{className:"headline"},["Hello World"]);
<h1className="headline">HelloWorld</h1>// will be transformed toReact.createElement("h1",{className:"headline"},["Hello World"]);
如果使用嵌套组件,的第三个参数createElement将包含嵌套的工厂函数调用。比 JSX 更容易调用,并且 TypeScript在创建新元素时React.createElement不会诉诸全局命名空间。听起来像是一个满足我们需求的完美解决方案。JSX
If you use nested components, the third parameter of createElement will contain nested factory function calls. React.createElement is much easier to call than JSX, and TypeScript won’t resort to the global JSX namespace when creating new elements. Sounds like a perfect workaround for our needs.
React.createElement需要三个参数:组件、props 和 children。目前,我们已经用 偷运了所有子组件props,但对于React.createElement我们需要明确说明。这也意味着我们需要明确定义children。
React.createElement needs three arguments: the component, the props, and the children. Right now, we’ve smuggled all child components with props, but for React.createElement we need to be explicit. This also means that we need to explicitly define children.
为此,我们创建了一个WithChildren<T>辅助类型。它采用现有类型并以以下形式添加可选子类型React.ReactNode:
For that, we create a WithChildren<T> helper type. It takes an existing type and adds optional children in the form of React.ReactNode:
typeWithChildren<T={}>=T&{children?:React.ReactNode};
typeWithChildren<T={}>=T&{children?:React.ReactNode};
WithChildren非常灵活。我们可以用它来包装我们的 props 类型:
WithChildren is highly flexible. We can wrap the type of our props with it:
typeCtaProps<TextendsCtaElements>=WithChildren<{as:T;}&JSX.IntrinsicElements[T]>;
typeCtaProps<TextendsCtaElements>=WithChildren<{as:T;}&JSX.IntrinsicElements[T]>;
或者我们可以创建一个联合:
Or we can create a union:
typeCtaProps<TextendsCtaElements>={as:T;}&JSX.IntrinsicElements[T]&WithChildren;
typeCtaProps<TextendsCtaElements>={as:T;}&JSX.IntrinsicElements[T]&WithChildren;
由于默认T设置为{},该类型变得普遍可用。这让您在需要时可以更轻松地附加children它们。下一步,我们将解构children并将props所有参数传递给React.createElement:
Since T is set to {} by default, the type becomes universally usable. This makes it a lot easier for you to attach children whenever you need them. As a next step, we destructure children out of props and pass all arguments into React.createElement:
functionCta<TextendsCtaElements>({as:Component,children,...props}:CtaProps<T>){returnReact.createElement(Component,props,children);}
functionCta<TextendsCtaElements>({as:Component,children,...props}:CtaProps<T>){returnReact.createElement(Component,props,children);}
And with that, your polymorphic component accepts the right parameters without any errors.
2012 年 TypeScript 首次发布时,JavaScript 生态系统和 JavaScript 语言的功能与今天完全无法相比。TypeScript 不仅以类型系统的形式引入了许多功能,还引入了语法,丰富了现有语言,使其能够跨模块、命名空间和类型抽象部分代码。
When TypeScript was released for the very first time in 2012, the JavaScript ecosystem and the features of the JavaScript language were not comparable to what we have today. TypeScript introduced many features not only in the form of a type system but also syntax, enriching an already existing language with possibilities to abstract parts of your code across modules, namespaces, and types.
这些功能之一就是类,这是面向对象编程中的主要内容。TypeScript 的类最初受到 C# 的很大影响,如果你了解这两种编程语言背后的人,这并不奇怪。1但它们也是基于已废弃的 ECMAScript 4 提案中的概念设计的。
One of these features was classes, a staple in object-oriented programming. TypeScript’s classes originally drew a lot of influence from C#, which is not surprising if you know the people behind both programming languages.1 But they are also designed based on concepts from the abandoned ECMAScript 4 proposals.
随着时间的推移,JavaScript 获得了 TypeScript 和其他语言开创的许多语言特性;类、私有字段、静态块和装饰器现在成为了 ECMAScript 标准的一部分,并已被运送到浏览器和服务器中的语言运行时。
Over time, JavaScript gained much of the language features pioneered by TypeScript and others; classes, along with private fields, static blocks, and decorators, are now part of the ECMAScript standard and have been shipped to language runtimes in the browser and the server.
这使得 TypeScript 处于早期语言创新与标准之间的最佳平衡点,TypeScript 团队将标准视为类型系统所有即将推出的功能的基准。虽然最初的设计与 JavaScript 的最终设计很接近,但仍有一些值得一提的差异。
This leaves TypeScript in a sweet spot between the innovation it brought to the language in the early days and standards, which is what the TypeScript team sees as a baseline for all upcoming features of the type system. While the original design is close to what JavaScript ended up with, there are some differences worth mentioning.
在本章中,我们将了解 TypeScript 和 JavaScript 中的类的行为方式、我们表达自己的可能性以及标准和原始设计之间的差异。我们将研究关键字、类型和泛型,并训练我们的眼睛来发现 TypeScript 为 JavaScript 添加的内容以及 JavaScript 本身带来了什么。
In this chapter, we look at how classes behave in TypeScript and JavaScript, the possibilities we have to express ourselves, and the differences between the standard and the original design. We look at keywords, types, and generics, and we train an eye to spot what’s being added by TypeScript to JavaScript, and what JavaScript brings to the table on its own.
TypeScript 中有两种属性可见性和访问权限:一种是通过特殊关键字语法public— , protected, private—,另一种是通过实际的 JavaScript 语法,此时属性以井号开头。您应该选择哪一种?
There are two flavors in TypeScript for property visibility and access: one through special keyword syntax—public, protected, private—and another one through actual JavaScript syntax, when properties start with a hash character. Which one should you choose?
首选 JavaScript 原生语法,因为它在运行时会产生一些您不想错过的影响。如果您依赖涉及各种可见性修饰符的复杂设置,请继续使用 TypeScript 修饰符。它们不会消失。
Prefer JavaScript-native syntax as it has some implications at runtime that you don’t want to miss. If you rely on a complex setup that involves variations of visibility modifiers, stay with the TypeScript ones. They won’t go away.
TypeScript 的类已经存在很长一段时间了,虽然它们从几年后出现的 ECMAScript 类中汲取了大量灵感,但 TypeScript 团队还决定引入当时在传统的基于类的面向对象编程中有用且流行的功能。
TypeScript’s classes have been around for quite a while, and while they draw huge inspiration from ECMAScript classes that followed a few years after, the TypeScript team also decided to introduce features that were useful and popular in traditional class-based object-oriented programming at the time.
这些功能之一是属性可见性修饰符,也称为访问修饰符。可见性修饰符是可以放在成员(属性和方法)前面的特殊关键字,用于告诉编译器如何从软件的其他部分查看和访问它们。
One of those features is property visibility modifiers, also referred to as access modifiers. Visibility modifiers are special keywords you can put in front of members—properties and methods—to tell the compiler how they can be seen and accessed from other parts of your software.
所有可见性修饰符以及 JavaScript 私有字段均适用于方法和属性。
All visibility modifiers, as well as JavaScript private fields, work on methods as well as properties.
默认可见性修饰符是public,可以明确写出或省略
:
The default visibility modifier is public, which can be written explicitly or just
omitted:
classPerson{publicname;// modifier public is optionalconstructor(name:string){this.name=name;}}constmyName=newPerson("Stefan").name;// works
classPerson{publicname;// modifier public is optionalconstructor(name:string){this.name=name;}}constmyName=newPerson("Stefan").name;// works
另一个修饰符是protected,限制类和子类的可见性:
Another modifier is protected, limiting visibility to classes and subclasses:
classPerson{protectedname;constructor(name:string){this.name=name;}getName(){// access worksreturnthis.name;}}constmyName=newPerson("Stefan").name;// ^// Property 'name' is private and only accessible within// class 'Person'.(2341)classTeacherextendsPerson{constructor(name:string){super(name);}getFullName(){// access worksreturn`Professor${this.name}`;}}
classPerson{protectedname;constructor(name:string){this.name=name;}getName(){// access worksreturnthis.name;}}constmyName=newPerson("Stefan").name;// ^// Property 'name' is private and only accessible within// class 'Person'.(2341)classTeacherextendsPerson{constructor(name:string){super(name);}getFullName(){// access worksreturn`Professor${this.name}`;}}
protected可以在派生类中改写 access 以代替public。access
protected还禁止从不属于同一子类的类引用访问成员。因此,虽然这有效:
protected access can be overwritten in derived classes to be public instead.
protected access also prohibits accessing members from class references that are not from the same subclass. So while this works:
classPlayerextendsPerson{constructor(name:string){super(name);}pair(p:Player){// worksreturn`Pairing${this.name}with${p.name}`;}}
classPlayerextendsPerson{constructor(name:string){super(name);}pair(p:Player){// worksreturn`Pairing${this.name}with${p.name}`;}}
使用基类或不同的子类将不起作用:
using the base class or a different subclass won’t work:
classPlayerextendsPerson{constructor(name:string){super(name);}pair(p:Person){return`Pairing${this.name}with${p.name}`;// ^// Property 'name' is protected and only accessible through an// instance of class 'Player'. This is an instance of// class 'Person'.(2446)}}
classPlayerextendsPerson{constructor(name:string){super(name);}pair(p:Person){return`Pairing${this.name}with${p.name}`;// ^// Property 'name' is protected and only accessible through an// instance of class 'Player'. This is an instance of// class 'Person'.(2446)}}
最后一个可见性修饰符是private,它只允许从同一个类内部进行访问:
The last visibility modifier is private, which allows access only from within the same class:
classPerson{privatename;constructor(name:string){this.name=name;}}constmyName=newPerson("Stefan").name;// ^// Property 'name' is protected and only accessible within// class 'Person' and its subclasses.(2445)classTeacherextendsPerson{constructor(name:string){super(name);}getFullName(){return`Professor${this.name}`;// ^// Property 'name' is private and only accessible// within class 'Person'.(2341)}}
classPerson{privatename;constructor(name:string){this.name=name;}}constmyName=newPerson("Stefan").name;// ^// Property 'name' is protected and only accessible within// class 'Person' and its subclasses.(2445)classTeacherextendsPerson{constructor(name:string){super(name);}getFullName(){return`Professor${this.name}`;// ^// Property 'name' is private and only accessible// within class 'Person'.(2341)}}
可见性修饰符也可以在构造函数中使用,作为定义属性和初始化属性的快捷方式:
Visibility modifiers also can be used in constructors as a shortcut to define properties and initialize them:
classCategory{constructor(publictitle:string,publicid:number,privatereference:bigint){}}// transpiles toclassCategory{constructor(title,id,reference){this.title=title;this.id=id;this.reference=reference;}}
classCategory{constructor(publictitle:string,publicid:number,privatereference:bigint){}}// transpiles toclassCategory{constructor(title,id,reference){this.title=title;this.id=id;this.reference=reference;}}
鉴于这里描述的所有功能,需要注意的是,TypeScript 的可见性修饰符是编译时注释,在编译步骤后会被删除。通常,如果整个属性声明不是通过类描述初始化,而是在构造函数中初始化,那么它们都会被删除,就像我们在上一个示例中看到的那样。
With all the features described here, it should be noted that TypeScript’s visibility modifiers are compile-time annotations that get erased after the compilation step. Often, entire property declarations get removed if they are not initialized via the class description but in the constructor, as we saw in the last example.
它们也仅在编译时检查期间有效,这意味着privateTypeScript 中的属性随后将在 JavaScript 中完全可访问;因此,您可以private通过断言实例来绕过访问检查as any,或者在代码编译后直接访问它们。它们也是可枚举的,这意味着它们的名称和值在通过JSON.stringify或序列化时变得可见Object.getOwnPropertyNames。简而言之:它们离开类型系统边界的那一刻,它们的行为就像常规 JavaScript 类成员一样。
They are also valid only during compile-time checks, meaning that a private property in TypeScript will be fully accessible in JavaScript afterward; thus, you can bypass the private access check by asserting your instances as any, or access them directly once your code has been compiled. They are also enumerable, which means that their names and values become visible when being serialized via JSON.stringify or Object.getOwnPropertyNames. In short: the moment they leave the boundaries of the type system they behave like regular JavaScript class members.
除了可见性修饰符之外,还可以向类属性添加readonly
修饰符。
Next to visibility modifiers, it’s also possible to add readonly
modifiers to class properties.
由于对属性的有限访问不仅在类型系统内合理,ECMAScript对常规 JavaScript 类采用了类似的概念,称为私有字段。
Since limited access to properties is a feature that is reasonable not only within a type system, ECMAScript has adopted a similar concept called private fields for regular JavaScript classes.
私有字段实际上不是使用可见性修饰符,而是在成员名称前面以井号或哈希的形式引入了新的语法。
Instead of a visibility modifier, private fields actually introduce new syntax in the form of a pound sign or hash in front of the member’s name.
引入私有字段的新语法在社区中引发了关于井号符号的愉悦感和美观度的激烈争论。一些参与者甚至称其为令人厌恶。如果这种添加也让您感到不快,那么将井号符号想象成一道小栅栏,放在您不想让所有人访问的东西前面可能会有所帮助。突然间,井号符号语法变得令人愉悦多了。
Introducing a new syntax for private fields has resulted in heated debate within the community on the pleasance and aesthetics of the pound sign. Some participants even called them abominable. If this addition irritates you as well, it might help to think of the pound sign as a little fence that you put in front of the things you don’t want everybody to have access to. Suddenly, the pound sign syntax becomes a lot more pleasant.
井号符号成为属性名称的一部分,这意味着也需要使用其前面的符号来访问它:
The pound sign becomes a part of the property’s name, meaning that it also needs to be accessed with the sign in front of it:
classPerson{#name:string;constructor(name:string){this.#name=name;}// we can use getters!getname():string{returnthis.#name.toUpperCase();}}constme=newPerson("Stefan");console.log(me.#name);// ^// Property '#name' is not accessible outside// class 'Person' because it has a private identifier.(18013)console.log(me.name);// works
classPerson{#name:string;constructor(name:string){this.#name=name;}// we can use getters!getname():string{returnthis.#name.toUpperCase();}}constme=newPerson("Stefan");console.log(me.#name);// ^// Property '#name' is not accessible outside// class 'Person' because it has a private identifier.(18013)console.log(me.name);// works
私有字段是彻头彻尾的 JavaScript;TypeScript 编译器不会删除任何内容,即使在编译步骤之后,它们仍保留其功能(将信息隐藏在类内)。以最新的 ECMAScript 版本为目标的转译结果看起来与 TypeScript 版本几乎相同,只是没有类型注释:
Private fields are JavaScript through and through; there is nothing the TypeScript compiler will remove, and they retain their functionality—hiding information inside the class—even after the compilation step. The transpiled result, with the latest ECMAScript version as a target, looks almost identical to the TypeScript version, just without type annotations:
classPerson{#name;constructor(name){this.#name=name;}getname(){returnthis.#name.toUpperCase();}}
classPerson{#name;constructor(name){this.#name=name;}getname(){returnthis.#name.toUpperCase();}}
私有字段不能在运行时代码中访问,并且它们也是不可枚举的,这意味着它们的内容信息不会以任何方式泄露。
Private fields can’t be accessed in runtime code, and they are also not enumerable, meaning that no information of their contents will be leaked in any way.
现在的问题是,TypeScript 中既有私有可见性修饰符,也有私有字段。可见性修饰符一直存在,并且与protected成员结合的方式更加多样。另一方面,私有字段与 JavaScript 尽可能接近,并且 TypeScript 的目标是成为“类型的 JavaScript 语法”,因此它们几乎符合该语言的长期计划。那么你应该选择哪一个呢?
The problem is now that both private visibility modifiers and private fields exist in TypeScript. Visibility modifiers have been there forever and have more variety combined with protected members. Private fields, on the other hand, are as close to JavaScript as they can get, and with TypeScript’s goal to be a “JavaScript syntax for types,” they pretty much hit the mark when it comes to the long-term plans of the language. So which one should you choose?
首先,无论你选择哪种修饰符,它们都能实现各自的目标,即在编译时告诉你在不应该访问属性的地方进行了访问。这是你收到的第一个反馈,告知你可能出现了问题,这也是我们使用 TypeScript 时的目标。因此,如果你需要向外部隐藏信息,那么每个工具都可以完成它的工作。
First, no matter which modifier you choose, they both fulfill their goal of telling you at compile time when there’s property access where it shouldn’t be. This is the first feedback you get informing you that something might be wrong, and this is what we’re aiming for when we use TypeScript. So if you need to hide information from the outside, every tool does its job.
但如果你进一步研究,就会发现这又取决于你的设置。如果你已经设置了一个具有详细可见性规则的项目,你可能无法立即将它们迁移到原生 JavaScript 版本。此外,JavaScriptprotected中缺乏可见性
可能会对你的目标造成问题。如果你已经拥有的东西已经有效,就没有必要改变一些东西。
But when you look further, it again depends on your setting. If you already set up a project with elaborate visibility rules, you might not be able to migrate them to the native JavaScript version immediately. Also, the lack of protected visibility in
JavaScript might be problematic for your goals. There is no need to change something if what you have already works.
如果您在运行时可见性方面遇到问题,无法显示您想要隐藏的详细信息:如果您依赖其他人使用您的代码作为库,并且他们不应该能够访问所有内部信息,那么私有字段就是您的最佳选择。它们在浏览器和其他语言运行时中得到很好的支持,并且 TypeScript 附带了适用于较旧平台的 polyfill。
If you run into problems with the runtime visibility showing details you want to hide: if you depend on others using your code as a library and they should not be able to access all the internal information, then private fields are the way to go. They are well-supported in browsers and other language runtimes, and TypeScript comes with polyfills for older platforms.
在类层次结构中,您从基类扩展并覆盖子类中的特定方法。重构基类时,您可能最终会保留旧的、未使用的方法,因为没有任何信息告诉您基类已发生更改。
In your class hierarchy, you extend from base classes and override specific methods in subclasses. When you refactor the base class, you might end up carrying around old, unused methods because nothing tells you that the base class has changed.
打开noImplicitOverride标志并使用override关键字来发出覆盖信号。
Switch on the noImplicitOverride flag and use the override keyword to signal overrides.
您想在画布上绘制形状。您的软件能够获取具有x和y坐标的点集合,并根据特定的渲染函数在 HTML 画布上绘制多边形、矩形或其他元素。
You want to draw shapes on a canvas. Your software is able to take a collection of points with x and y coordinates, and based on a specific render function, it will draw either polygons, rectangles, or other elements on an HTML canvas.
您决定采用类层次结构,其中基类Shape采用任意元素列表Point并在它们之间画线。此类通过 setter 和 getter 负责内部管理,但也实现render函数本身:
You decide to go for a class hierarchy, where the base class Shape takes an arbitrary list of Point elements and draws lines between them. This class takes care of housekeeping through setters and getters but also implements the render function itself:
typePoint={x:number;y:number;};classShape{points:Point[];fillStyle:string="white";lineWidth:number=10;constructor(points:Point[]){this.points=points;}setfill(style:string){this.fillStyle=style;}setwidth(width:number){this.lineWidth=width;}render(ctx:CanvasRenderingContext2D){if(this.points.length){ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;ctx.beginPath();letpoint=this.points[0];ctx.moveTo(point.x,point.y);for(leti=1;i<this.points.length;i++){point=this.points[i];ctx.lineTo(point.x,point.y);}ctx.closePath();ctx.stroke();}}}
typePoint={x:number;y:number;};classShape{points:Point[];fillStyle:string="white";lineWidth:number=10;constructor(points:Point[]){this.points=points;}setfill(style:string){this.fillStyle=style;}setwidth(width:number){this.lineWidth=width;}render(ctx:CanvasRenderingContext2D){if(this.points.length){ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;ctx.beginPath();letpoint=this.points[0];ctx.moveTo(point.x,point.y);for(leti=1;i<this.points.length;i++){point=this.points[i];ctx.lineTo(point.x,point.y);}ctx.closePath();ctx.stroke();}}}
要使用它,请从 HTML 画布元素创建一个 2D 上下文,创建一个新的实例Shape,然后将上下文传递给函数render:
To use it, create a 2D context from an HTML canvas element, create a new instance of Shape, and pass the context to the render function:
constcanvas=document.getElementsByTagName("canvas")[0];constctx=canvas?.getContext("2d");constshape=newShape([{x:50,y:140},{x:150,y:60},{x:250,y:140},]);shape.fill="red";shape.width=20;shape.render(ctx);
constcanvas=document.getElementsByTagName("canvas")[0];constctx=canvas?.getContext("2d");constshape=newShape([{x:50,y:140},{x:150,y:60},{x:250,y:140},]);shape.fill="red";shape.width=20;shape.render(ctx);
现在我们要使用已建立的基类并派生特定形状(如矩形)的子类。我们保留了管理方法,并特别重写了constructor以及render方法:
Now we want to use the established base class and derive subclasses for specific shapes, like rectangles. We keep the housekeeping methods and specifically override the constructor, as well as the render method:
classRectangleextendsShape{constructor(points:Point[]){if(points.length!==2){throwError(`Wrong number of points, expected 2, got${points.length}`);}super(points);}render(ctx:CanvasRenderingContext2D){ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;leta=this.points[0];letb=this.points[1];ctx.strokeRect(a.x,a.y,b.x-a.x,b.y-a.y);}}
classRectangleextendsShape{constructor(points:Point[]){if(points.length!==2){throwError(`Wrong number of points, expected 2, got${points.length}`);}super(points);}render(ctx:CanvasRenderingContext2D){ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;leta=this.points[0];letb=this.points[1];ctx.strokeRect(a.x,a.y,b.x-a.x,b.y-a.y);}}
用法Rectangle几乎相同:
The usage of Rectangle is pretty much the same:
constrectangle=newRectangle([{x:130,y:190},{x:170,y:250}]);rectangle.render(ctx);
constrectangle=newRectangle([{x:130,y:190},{x:170,y:250}]);rectangle.render(ctx);
随着软件的发展,我们不可避免地会改变类、方法和函数,并且代码库中的某些人会将render方法重命名为draw:
As our software evolves, we inevitably change classes, methods, and functions, and somebody in our codebase will rename the render method to draw:
classShape{// see abovedraw(ctx:CanvasRenderingContext2D){if(this.points.length){ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;ctx.beginPath();letpoint=this.points[0];ctx.moveTo(point.x,point.y);for(leti=1;i<this.points.length;i++){point=this.points[i];ctx.lineTo(point.x,point.y);}ctx.closePath();ctx.stroke();}}}
classShape{// see abovedraw(ctx:CanvasRenderingContext2D){if(this.points.length){ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;ctx.beginPath();letpoint=this.points[0];ctx.moveTo(point.x,point.y);for(leti=1;i<this.points.length;i++){point=this.points[i];ctx.lineTo(point.x,point.y);}ctx.closePath();ctx.stroke();}}}
这本身并不是问题,但如果我们没有在代码中的任何地方使用render方法,也许是因为我们将该软件发布为一个库并且没有在测试中使用它,那么没有任何信息告诉我们该方法仍然存在,并且与原始类没有任何联系。RectanglerenderRectangle
This is not a problem per se, but if we are not using the render method of Rectangle anywhere in our code, perhaps because we publish this software as a library and didn’t use it in our tests, nothing tells us that the render method in Rectangle still exists, with no connection to the original class whatsoever.
这就是为什么 TypeScript 允许你使用关键字注释要覆盖的方法override。这是 TypeScript 的语法扩展,在 TypeScript 将代码转换为 JavaScript 时将被删除。
This is why TypeScript allows you to annotate methods you want to override with the override keyword. This is a syntax extension from TypeScript and will be removed the moment TypeScript transpiles your code to JavaScript.
当使用关键字标记方法时override,TypeScript 将确保基类中存在具有相同名称和签名的方法。如果将其重命名render为draw,TypeScript 将告诉您该方法render未在基类中声明Shape:
When a method is marked with the override keyword, TypeScript will make sure that a method of the same name and signature exists in the base class. If you rename render to draw, TypeScript will tell you that the method render wasn’t declared in the base class Shape:
classRectangleextendsShape{// see aboveoverriderender(ctx:CanvasRenderingContext2D){// ^// This member cannot have an 'override' modifier because it// is not declared in the base class 'Shape'.(4113)ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;leta=this.points[0];letb=this.points[1];ctx.strokeRect(a.x,a.y,b.x-a.x,b.y-a.y);}}
classRectangleextendsShape{// see aboveoverriderender(ctx:CanvasRenderingContext2D){// ^// This member cannot have an 'override' modifier because it// is not declared in the base class 'Shape'.(4113)ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;leta=this.points[0];letb=this.points[1];ctx.strokeRect(a.x,a.y,b.x-a.x,b.y-a.y);}}
此错误是一个很好的保障,可以确保重命名和重构不会破坏您现有的合同。
This error is a great safeguard to ensure that renames and refactors don’t break your existing contracts.
尽管constructor可以看作是一个被重写的方法,但其语义是不同的,并通过其他规则处理(例如,确保super在实例化子类时调用)。
Even though a constructor could be seen as an overridden method, its semantics are different and handled through other rules (for example, making sure that you call super when instantiating a subclass).
noImplicitOverrides通过在tsconfig.json中打开该标志,你可以进一步确保需要使用关键字标记函数override。否则,TypeScript 将引发另一个错误:
By switching on the noImplicitOverrides flag in your tsconfig.json, you can further ensure that you need to mark functions with the override keyword. Otherwise, TypeScript will throw another error:
classRectangleextendsShape{// see abovedraw(ctx:CanvasRenderingContext2D){// ^// This member must have an 'override' modifier because it// overrides a member in the base class 'Shape'.(4114)ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;leta=this.points[0];letb=this.points[1];ctx.strokeRect(a.x,a.y,b.x-a.x,b.y-a.y);}}
classRectangleextendsShape{// see abovedraw(ctx:CanvasRenderingContext2D){// ^// This member must have an 'override' modifier because it// overrides a member in the base class 'Shape'.(4114)ctx.fillStyle=this.fillStyle;ctx.lineWidth=this.lineWidth;leta=this.points[0];letb=this.points[1];ctx.strokeRect(a.x,a.y,b.x-a.x,b.y-a.y);}}
实现定义类基本形状的接口等技术已经提供了坚实的基础,可以防止您遇到此类问题。因此,在创建类层次结构时,最好将关键字override和noImplictOverrides作为额外的保护措施
。
Techniques like implementing interfaces that define the basic shape of a class already provide a solid baseline to prevent you from running into problems like this. So, it’s good to see the override keyword and noImplictOverrides as additional safeguards when
creating class hierarchies.
当您的软件需要依赖类层次结构才能工作时,override一起使用noImplicitAny是确保您不会忘记任何事情的好方法。类层次结构与任何层次结构一样,随着时间的推移会变得复杂,因此请采取任何可能的保护措施。
When your software needs to rely on class hierarchies to work, using override together with noImplicitAny is a good way to ensure that you don’t forget anything. Class hierarchies, like any hierarchies, tend to grow complicated over time, so take any safeguard you can get.
使用构造函数接口模式描述你的类。
Describe your classes with the constructor interface pattern.
如果您在 TypeScript 中使用类层次结构,TypeScript 的结构特性有时会妨碍您。例如,查看以下类层次结构,我们想要根据不同的规则过滤一组元素:
If you use class hierarchies with TypeScript, the structural features of TypeScript sometimes get in your way. Look at the following class hierarchy for instance, where we want to filter a set of elements based on different rules:
abstractclassFilterItem{constructor(privateproperty:string){};someFunction(){/* ... */};abstractfilter():void;}classAFilterextendsFilterItem{filter(){/* ... */}}classBFilterextendsFilterItem{filter(){/* ... */}}
abstractclassFilterItem{constructor(privateproperty:string){};someFunction(){/* ... */};abstractfilter():void;}classAFilterextendsFilterItem{filter(){/* ... */}}classBFilterextendsFilterItem{filter(){/* ... */}}
抽象类FilterItem需要由其他类实现。在此示例中,AFilter和BFilter都是的具体化FilterItem,它们作为过滤器的基准:
The FilterItem abstract class needs to be implemented by other classes. In this example AFilter and BFilter, both concretizations of FilterItem, serve as a baseline for filters:
constsome:FilterItem=newAFilter('afilter');// ok
constsome:FilterItem=newAFilter('afilter');// ok
当我们不立即使用实例时,事情会变得有趣。假设我们想要根据从 AJAX 调用中获得的令牌实例化新过滤器。为了让我们更容易选择过滤器,我们将所有可能的过滤器存储在一个映射中:
Things get interesting when we are not working with instances right off the bat. Let’s say we want to instantiate new filters based on a token we get from an AJAX call. To make it easier for us to select the filter, we store all possible filters in a map:
declareconstfilterMap:Map<string,typeofFilterItem>;filterMap.set('number',AFilter);filterMap.set('stuff',BFilter);
declareconstfilterMap:Map<string,typeofFilterItem>;filterMap.set('number',AFilter);filterMap.set('stuff',BFilter);
映射的泛型设置为 a string(用于来自后端的令牌)以及补充 类型签名的所有内容FilterItem。我们在这里使用typeof关键字是为了能够将类而不是对象添加到映射中。毕竟,我们希望事后实例化它们。
The map’s generics are set to a string (for the token from the backend) and everything that complements the type signature of FilterItem. We use the typeof keyword here to be able to add classes to the map, not objects. We want to instantiate them afterward, after all.
到目前为止,一切都按预期运行。当您想要从映射中获取一个类并使用它创建一个新对象时,就会出现问题:
So far everything works as you would expect. The problem occurs when you want to fetch a class from the map and create a new object with it:
letobj:FilterItem;// get the constructorconstctor=filterMap.get('number');if(typeofctor!=='undefined'){obj=newctor();// ^// cannot create an object of an abstract class}
letobj:FilterItem;// get the constructorconstctor=filterMap.get('number');if(typeofctor!=='undefined'){obj=newctor();// ^// cannot create an object of an abstract class}
这是个问题!TypeScript 目前只知道我们得到了一个FilterItem返回值,但无法实例化FilterItem。抽象类将类型信息(类型命名空间)与实际实现(值命名空间)混合在一起。作为第一步,让我们看看类型:我们期望从中得到什么filterMap?让我们创建一个接口(或类型别名)来定义应该是什么样子FilterItem:
This is a problem! TypeScript only knows at this point that we get a FilterItem back and we can’t instantiate FilterItem. Abstract classes mix type information (type namespace) with an actual implementation (value namespace). As a first step, let’s just look at the types: what are we expecting to get back from filterMap? Let’s create an interface (or type alias) that defines how the shape of FilterItem should look:
interfaceIFilter{new(property:string):IFilter;someFunction():void;filter():void;}declareconstfilterMap:Map<string,IFilter>;
interfaceIFilter{new(property:string):IFilter;someFunction():void;filter():void;}declareconstfilterMap:Map<string,IFilter>;
注意new关键字。这是 TypeScript 定义构造函数类型签名的一种方法。如果我们用实际接口替换抽象类,就会出现大量错误。无论你将命令放在哪里implements IFilter,似乎都没有实现能够满足我们的合同:
Note the new keyword. This is a way for TypeScript to define the type signature of a constructor function. If we substitute the abstract class for an actual interface, lots of errors start appearing. No matter where you put the implements IFilter command, no implementation seems to satisfy our contract:
abstractclassFilterItemimplementsIFilter{/* ... */}// ^// Class 'FilterItem' incorrectly implements interface 'IFilter'.// Type 'FilterItem' provides no match for the signature// 'new (property: string): IFilter'.filterMap.set('number',AFilter);// ^// Argument of type 'typeof AFilter' is not assignable// to parameter of type 'IFilter'. Type 'typeof AFilter' is missing// the following properties from type 'IFilter': someFunction, filter
abstractclassFilterItemimplementsIFilter{/* ... */}// ^// Class 'FilterItem' incorrectly implements interface 'IFilter'.// Type 'FilterItem' provides no match for the signature// 'new (property: string): IFilter'.filterMap.set('number',AFilter);// ^// Argument of type 'typeof AFilter' is not assignable// to parameter of type 'IFilter'. Type 'typeof AFilter' is missing// the following properties from type 'IFilter': someFunction, filter
这是怎么回事?似乎实现和类本身都无法获取我们在接口声明中定义的所有属性和函数。为什么?
What’s happening here? It seems like neither the implementation nor the class itself can get all the properties and functions we’ve defined in our interface declaration. Why?
JavaScript 类比较特殊;它们不只有一种我们可以轻松定义的类型,而是两种:静态端的类型和实例端的类型。如果我们将类转换为 ES6 之前的类型,即构造函数和 原型,可能会更清楚:
JavaScript classes are special; they have not just one type we could easily define but two: the type of the static side and the type of the instance side. It might be clearer if we transpile our class to what it was before ES6, a constructor function and a prototype:
functionAFilter(property){// this is part of the static sidethis.property=property;// this is part of the instance side}// a function of the instance sideAFilter.prototype.filter=function(){/* ... */}// not part of our example, but on the static sideAfilter.something=function(){/* ... */}
functionAFilter(property){// this is part of the static sidethis.property=property;// this is part of the instance side}// a function of the instance sideAFilter.prototype.filter=function(){/* ... */}// not part of our example, but on the static sideAfilter.something=function(){/* ... */}
一种类型用于创建对象。一种类型用于对象本身。因此,让我们将其拆分并为其创建两个类型声明:
One type to create the object. One type for the object itself. So let’s split it up and create two type declarations for it:
interfaceFilterConstructor{new(property:string):IFilter;}interfaceIFilter{someFunction():void;filter():void;}
interfaceFilterConstructor{new(property:string):IFilter;}interfaceIFilter{someFunction():void;filter():void;}
第一种类型,FilterConstructor,是构造函数接口。这里是所有静态属性和构造函数本身。构造函数返回一个实例:IFilter。IFilter包含实例端的类型信息。我们声明的所有函数。
The first type, FilterConstructor, is the constructor interface. Here are all static properties and the constructor function itself. The constructor function returns an instance: IFilter. IFilter contains type information of the instance side. All the functions we declare.
通过拆分,我们后续的类型也会变得更加清晰:
By splitting this up, our subsequent typings also become a lot clearer:
declareconstfilterMap:Map<string,FilterConstructor>;/* 1 */filterMap.set('number',AFilter);filterMap.set('stuff',BFilter);letobj:IFilter;/* 2 */constctor=filterMap.get('number');if(typeofctor!=='undefined'){obj=newctor('a');}
declareconstfilterMap:Map<string,FilterConstructor>;/* 1 */filterMap.set('number',AFilter);filterMap.set('stuff',BFilter);letobj:IFilter;/* 2 */constctor=filterMap.get('number');if(typeofctor!=='undefined'){obj=newctor('a');}
我们将类型的实例添加FilterConstructor到映射中。这意味着我们只能添加生成所需对象的类。
We add instances of type FilterConstructor to our map. This means we only can add classes that produce the desired objects.
我们最终想要的是 的一个实例IFilter。这就是构造函数在使用 调用时返回的内容new。
What we want in the end is an instance of IFilter. This is what the constructor function returns when being called with new.
我们的代码再次编译,我们得到了我们想要的所有自动完成和工具。更棒的是,我们无法将抽象类添加到映射中,因为它们不会生成有效的实例:
Our code compiles again, and we get all the autocompletion and tooling we desire. Even better, we are not able to add abstract classes to the map because they don’t produce a valid instance:
filterMap.set('notworking',FilterItem);// ^// Cannot assign an abstract constructor type to a// non-abstract constructor type.
filterMap.set('notworking',FilterItem);// ^// Cannot assign an abstract constructor type to a// non-abstract constructor type.
构造函数接口模式在 TypeScript 和标准库中广泛使用。要了解其含义,请查看lib.es5.d.tsObjectContructor中的接口。
The constructor interface pattern is used throughout TypeScript and the standard library. To get an idea, look at the ObjectContructor interface from lib.es5.d.ts.
如果您无法从参数中推断出泛型,请在实例化时明确注释泛型;否则,它们将默认为unknown并接受广泛的值。使用泛型约束和默认参数可提高安全性。
Explicitly annotate generics at instantiation if you can’t infer them from your parameters; otherwise, they default to unknown and accept a broad range of values. Use generic constraints and default parameters for extra safety.
类也允许使用泛型。我们不仅可以向函数添加泛型类型参数,还可以向类添加泛型类型参数。虽然类方法的泛型类型参数仅在函数范围内有效,但类的泛型类型参数对整个类都有效。
Classes also allow for generics. Instead of only being able to add generic type parameters to functions, we can also add generic type parameters to classes. While generic type parameters at class methods are valid only in function scope, generic type parameters for classes are valid for the entirety of a class.
让我们创建一个集合,一个使用一组有限的便利函数对数组进行简单的包装。我们可以将此类型参数添加T到类定义中Collection,并在整个类中重复使用:
Let’s create a collection, a simple wrapper around an array with a restricted set of convenience functions. We can add T to the class definition of Collection and reuse this type parameter throughout the entire class:
classCollection<T>{items:T[];constructor(){this.items=[];}add(item:T){this.items.push(item);}contains(item:T):boolean{returnthis.items.includes(item);}}
classCollection<T>{items:T[];constructor(){this.items=[];}add(item:T){this.items.push(item);}contains(item:T):boolean{returnthis.items.includes(item);}}
这样,我们就可以T用泛型类型注释明确地替换,例如,允许仅收集数字或仅收集字符串:
With that, we are able to explicitly substitute T with a generic type annotation, for example, allowing a collection of only numbers or only strings:
constnumbers=newCollection<number>();numbers.add(1);numbers.add(2);conststrings=newCollection<string>();strings.add("Hello");strings.add("World");
constnumbers=newCollection<number>();numbers.add(1);numbers.add(2);conststrings=newCollection<string>();strings.add("Hello");strings.add("World");
作为开发人员,我们不需要明确注释泛型类型参数。TypeScript 通常会尝试根据用法推断泛型类型。如果我们忘记添加泛型类型参数,TypeScript 会回退到unknown,允许我们添加所有内容:
We as developers are not required to explicitly annotate generic type parameters. TypeScript usually tries to infer generic types from usage. If we forget to add a generic type parameter, TypeScript falls back to unknown, allowing us to add everything:
constunknowns=newCollection();unknowns.add(1);unknowns.add("World");
constunknowns=newCollection();unknowns.add(1);unknowns.add("World");
让我们停留在这一点上一秒钟。TypeScript 对我们非常诚实。当我们构造一个新的实例时Collection,我们不知道我们的项目是什么类型。unknown是对集合状态的最准确描述。它伴随着所有的缺点:我们可以添加任何东西,每次检索值时我们都需要进行类型检查。虽然 TypeScript 目前只做了可能的事情,但我们可能想做得更好。具体类型T是Collection正常工作所必需的。
Let’s stay at this point for a second. TypeScript is very honest with us. The moment we construct a new instance of Collection, we don’t know what the type of our items is. unknown is the most accurate depiction of the collection’s state. And it comes with all the downsides: we can add anything, and we need to do type-checks every time we retrieve a value. While TypeScript does the only thing possible at this point, we might want to do better. A concrete type for T is mandatory for Collection to properly work.
让我们看看我们是否可以依赖推断。TypeScript 对类的推断与对函数的推断一样。如果有某种类型的参数,TypeScript 将采用此类型并替换泛型类型参数。类旨在保持状态,状态在其使用过程中发生变化。状态还定义了我们的泛型类型参数T。为了正确推断T,我们需要在构造时需要一个参数,可能是初始值:
Let’s see if we can rely on inference. TypeScript’s inference on classes works just like it does on functions. If there is a parameter of a certain type, TypeScript will take this type and substitute the generic type parameter. Classes are designed to keep state, and state changes throughout their use. The state also defines our generic type parameter T. To correctly infer T, we need to require a parameter at construction, maybe an initial value:
classCollection<T>{items:T[];constructor(initial:T){this.items=[initial];}add(item:T){this.items.push(item);}contains(item:T):boolean{returnthis.items.includes(item);}}// T is number!constnumbersInf=newCollection(0);numbersInf.add(1);
classCollection<T>{items:T[];constructor(initial:T){this.items=[initial];}add(item:T){this.items.push(item);}contains(item:T):boolean{returnthis.items.includes(item);}}// T is number!constnumbersInf=newCollection(0);numbersInf.add(1);
这确实可行,但是我们的 API 设计还有很多不足之处。如果我们没有初始值怎么办?虽然其他类可能有可用于推理的参数,但对于各种项目的集合来说,这可能没有多大意义。
This works, but it leaves a lot to be desired for our API design. What if we don’t have initial values? While other classes might have parameters that can be used for inference, this might not make a lot of sense for a collection of various items.
对于Collection,通过注释提供类型是绝对必要的。剩下的唯一方法就是确保我们不会忘记添加注释。为了实现这一点,我们可以确保 TypeScript 的通用默认参数和底部类型never:
For Collection, it is absolutely essential to provide a type through annotation. The only way left is to ensure we don’t forget to add an annotation. To achieve this, we can make sure of TypeScript’s generic default parameters and the bottom type never:
classCollection<T=never>{items:T[];constructor(){this.items=[];}add(item:T){this.items.push(item);}contains(item:T):boolean{returnthis.items.includes(item);}}
classCollection<T=never>{items:T[];constructor(){this.items=[];}add(item:T){this.items.push(item);}contains(item:T):boolean{returnthis.items.includes(item);}}
我们将泛型类型参数T默认设置为never,这为我们的类添加了一些非常有趣的行为。T仍然可以通过注释显式地替换为每种类型,工作方式与以前一样,但是一旦我们忘记注释,类型就不是unknown,而是never。这意味着没有值与我们的集合兼容,导致我们尝试添加某些东西时出现许多错误:
We set the generic type parameter T to default to never, which adds some very interesting behavior to our class. T still can be explicitly substituted with every type through annotation, working just as before, but the moment we forget an annotation the type is not unknown, it’s never. Meaning that no value is compatible with our collection, resulting in many errors the moment we try to add something:
constnevers=newCollection();nevers.add(1);// ^// Argument of type 'number' is not assignable// to parameter of type 'never'.(2345)nevers.add("World");// ^// Argument of type 'string' is not assignable// to parameter of type 'never'.(2345)
constnevers=newCollection();nevers.add(1);// ^// Argument of type 'number' is not assignable// to parameter of type 'never'.(2345)nevers.add("World");// ^// Argument of type 'string' is not assignable// to parameter of type 'never'.(2345)
This fallback makes the use of our generic classes a lot safer.
坚持使用命名空间声明来进行附加类型声明,尽可能避免使用抽象类,并且优先使用 ECMAScript 模块而不是静态类。
Stick with namespace declarations for additional type declarations, avoid abstract classes when possible, and prefer ECMAScript modules instead of static classes.
我们从那些经常使用 Java 或 C# 等传统面向对象编程语言的人身上看到一个现象,那就是他们渴望将所有内容包装在类中。在 Java 中,你没有其他选择,因为类是构造代码的唯一方法。在 JavaScript(以及 TypeScript)中,有许多其他可能性可以做你想做的事情,而无需任何额外的步骤。其中之一就是静态类或具有静态方法的类:
One thing we see from people who worked a lot with traditional object-oriented programming languages like Java or C# is their urge to wrap everything inside a class. In Java, you don’t have any other options as classes are the only way to structure code. In JavaScript (and thus TypeScript), plenty of other possibilities do what you want without any extra steps. One of those is static classes or classes with static methods:
// Environment.tsexportdefaultclassEnvironment{privatestaticvariableList:string[]=[]staticvariables():string[]{/* ... */}staticsetVariable(key:string,value:any):void{/* ... */}staticgetValue(key:string):unknown{/* ... */}}// Usage in another fileimport*asEnvironmentfrom"./Environment";console.log(Environment.variables());
// Environment.tsexportdefaultclassEnvironment{privatestaticvariableList:string[]=[]staticvariables():string[]{/* ... */}staticsetVariable(key:string,value:any):void{/* ... */}staticgetValue(key:string):unknown{/* ... */}}// Usage in another fileimport*asEnvironmentfrom"./Environment";console.log(Environment.variables());
虽然这可以工作,甚至是有效的 JavaScript(没有类型注释),但对于一些很容易成为简单、无趣函数的东西来说,这太过繁琐了:
While this works and is even—sans type annotations—valid JavaScript, it’s way too much ceremony for something that can easily be just plain, boring functions:
// Environment.tsconstvariableList:string=[]exportfunctionvariables():string[]{/* ... */}exportfunctionsetVariable(key:string,value:any):void{/* ... */}exportfunctiongetValue(key:string):unknown{/* ... */}// Usage in another fileimport*asEnvironmentfrom"./Environment";console.log(Environment.variables());
// Environment.tsconstvariableList:string=[]exportfunctionvariables():string[]{/* ... */}exportfunctionsetVariable(key:string,value:any):void{/* ... */}exportfunctiongetValue(key:string):unknown{/* ... */}// Usage in another fileimport*asEnvironmentfrom"./Environment";console.log(Environment.variables());
用户的界面完全相同。您可以像访问类中的静态属性一样访问模块范围变量,但这些变量会自动进入模块范围。您可以决定导出什么以及使什么可见,而不是某些 TypeScript 字段修饰符。此外,您不会最终创建一个Environment不执行任何操作的实例。
The interface for your users is exactly the same. You can access module scope variables just the way you would access static properties in a class, but you have them module scoped automatically. You decide what to export and what to make visible, not some TypeScript field modifiers. Also, you don’t end up creating an Environment instance that doesn’t do anything.
甚至实现也变得更容易。查看类版本
variables():
Even the implementation becomes easier. Check out the class version of
variables():
exportdefaultclassEnvironment{privatestaticvariableList:string[]=[];staticvariables():string[]{returnthis.variableList;}}
exportdefaultclassEnvironment{privatestaticvariableList:string[]=[];staticvariables():string[]{returnthis.variableList;}}
与模块版本相反:
as opposed to the module version:
constvariableList:string=[]exportfunctionvariables():string[]{returnvariableList;}
constvariableList:string=[]exportfunctionvariables():string[]{returnvariableList;}
这并不this意味着要考虑的事情更少。作为额外的好处,你的打包器可以更轻松地进行 tree shake,因此你最终只会得到你实际使用的东西:
No this means less to think about. As an added benefit, your bundlers have an easier time doing tree shaking, so you end up with only the things you actually use:
// Only the variables function and variableList// end up in the bundleimport{variables}from"./Environment";console.log(variables());
// Only the variables function and variableList// end up in the bundleimport{variables}from"./Environment";console.log(variables());
这就是为什么我们总是优先选择合适的模块而不是带有静态字段和方法的类。那只是一个额外的样板,没有任何额外的好处。
That’s why a proper module is always preferred to a class with static fields and methods. That’s just an added boilerplate with no extra benefit.
与静态类一样,具有 Java 或 C# 背景的人会使用命名空间,这是 TypeScript 在 ECMAScript 模块标准化之前就引入的组织代码的功能。它们允许你将内容拆分到多个文件,然后使用引用标记再次合并它们:
As with static classes, people with a Java or C# background cling to namespaces, a feature that TypeScript introduced to organize code long before ECMAScript modules were standardized. They allowed you to split things across files, merging them again with reference markers:
// file users/models.tsnamespaceUsers{exportinterfacePerson{name:string;age:number;}}// file users/controller.ts/// <reference path="./models.ts" />namespaceUsers{exportfunctionupdateUser(p:Person){// do the rest}}
// file users/models.tsnamespaceUsers{exportinterfacePerson{name:string;age:number;}}// file users/controller.ts/// <reference path="./models.ts" />namespaceUsers{exportfunctionupdateUser(p:Person){// do the rest}}
那时,TypeScript 甚至有一个捆绑功能。它现在应该还能用。但如前所述,这是在 ECMAScript 引入模块之前。现在有了模块,我们就有办法组织和构建与 JavaScript 生态系统其他部分兼容的代码。这是一个优点。
Back then, TypeScript even had a bundling feature. It should still work. But as noted, this was before ECMAScript introduced modules. Now with modules, we have a way to organize and structure code that is compatible with the rest of the JavaScript ecosystem. And that’s a plus.
那么我们为什么需要命名空间呢?如果你想要扩展第三方依赖项(例如,位于节点模块内)的定义,命名空间仍然有效。假设你想扩展全局JSX命名空间并确保img元素具有 alt 文本:
So why do we need namespaces? Namespaces are still valid if you want to extend definitions from a third-party dependency, for example, that lives inside node modules. Say you want to extend the global JSX namespace and make sure img elements feature alt texts:
declarenamespaceJSX{interfaceIntrinsicElements{"img":HTMLAttributes&{alt:string;src:string;loading?:'lazy'|'eager'|'auto';}}}
declarenamespaceJSX{interfaceIntrinsicElements{"img":HTMLAttributes&{alt:string;src:string;loading?:'lazy'|'eager'|'auto';}}}
或者你想在环境模块中编写复杂的类型定义。但除此之外呢?它已经没什么用了。
Or you want to write elaborate type definitions in ambient modules. But other than that? There is not much use for it anymore.
命名空间将您的定义包装到对象中,写法如下:
Namespaces wrap your definitions into an object, writing something like this:
exportnamespaceUsers{typeUser={name:string;age:number;};exportfunctioncreateUser(name:string,age:number):User{return{name,age};}}
exportnamespaceUsers{typeUser={name:string;age:number;};exportfunctioncreateUser(name:string,age:number):User{return{name,age};}}
这会产生一些非常复杂的东西:
This emits something very elaborate:
exportvarUsers;(function(Users){functioncreateUser(name,age){return{name,age};}Users.createUser=createUser;})(Users||(Users={}));
exportvarUsers;(function(Users){functioncreateUser(name,age){return{name,age};}Users.createUser=createUser;})(Users||(Users={}));
这不仅增加了繁琐,而且还会使你的打包器无法正确地进行 tree shake!使用它们也会变得有点冗长:
This not only adds cruft but also keeps your bundlers from tree shaking properly! Using them also becomes a bit wordier:
import*asUsersfrom"./users";Users.Users.createUser("Stefan","39");
import*asUsersfrom"./users";Users.Users.createUser("Stefan","39");
删除它们会让事情变得简单很多。坚持使用 JavaScript 提供的功能。在声明文件之外不使用命名空间会使您的代码清晰、简单且整洁。
Dropping them makes things a lot easier. Stick to what JavaScript offers. Not using namespaces outside of declaration files makes your code clear, simple, and tidy.
最后但并非最不重要的一点是,还有抽象类。抽象类是一种构造更复杂的类层次结构的方法,其中你可以预定义行为,但将某些功能的实际实现留给从抽象类扩展的类:
Last but not least, there are abstract classes. Abstract classes are a way to structure a more complex class hierarchy where you predefine a behavior but leave the actual implementation of some features to classes that extend from your abstract class:
abstractclassLifeform{age:number;constructor(age:number){this.age=age;}abstractmove():string;}classHumanextendsLifeform{move(){return"Walking, mostly...";}}
abstractclassLifeform{age:number;constructor(age:number){this.age=age;}abstractmove():string;}classHumanextendsLifeform{move(){return"Walking, mostly...";}}
所有 的子类都要Lifeform实现move。这个概念基本上存在于所有基于类的编程语言中。问题是 JavaScript 传统上不是基于类的。例如,像下面这样的抽象类会生成一个有效的 JavaScript 类,但不允许在 TypeScript 中实例化:
It’s for all subclasses of Lifeform to implement move. This is a concept that exists in basically every class-based programming language. The problem is that JavaScript isn’t traditionally class based. For example, an abstract class like the following generates a valid JavaScript class but is not allowed to be instantiated in TypeScript:
abstractclassLifeform{age:number;constructor(age:number){this.age=age;}}constlifeform=newLifeform(20);// ^// Cannot create an instance of an abstract class.(2511)
abstractclassLifeform{age:number;constructor(age:number){this.age=age;}}constlifeform=newLifeform(20);// ^// Cannot create an instance of an abstract class.(2511)
如果您编写的是常规 JavaScript,但依赖 TypeScript 以隐式文档的形式提供信息,这可能会导致一些不必要的情况,例如如果函数定义如下所示:
This can lead to some unwanted situations if you’re writing regular JavaScript but rely on TypeScript to provide the information in the form of implicit documentation, such as if a function definition looks like this:
declarefunctionmoveLifeform(lifeform:Lifeform);
declarefunctionmoveLifeform(lifeform:Lifeform);
Lifeform您或您的用户可能会将此解读为将对象传递给 的邀请moveLifeform。在内部,它调用lifeform.move()。
You or your users might read this as an invitation to pass a Lifeform object to moveLifeform. Internally, it calls lifeform.move().
Lifeform可以在 JavaScript 中实例化,因为它是一个有效的类。
Lifeform can be instantiated in JavaScript, as it is a valid class.
该方法move在 中不存在Lifeform,因此会破坏您的应用程序!
The method move does not exist in Lifeform, thus breaking your application!
这是由于一种虚假的安全感造成的。你真正想要的是将一些预定义的实现放入原型链中,并有一个契约告诉你会发生什么:
This is due to a false sense of security. What you actually want is to put some pre-defined implementation in the prototype chain and have a contract that tells you what to expect:
interfaceLifeform{move():string;}classBasicLifeForm{age:number;constructor(age:number){this.age=age;}}classHumanextendsBasicLifeFormimplementsLifeform{move(){return"Walking";}}
interfaceLifeform{move():string;}classBasicLifeForm{age:number;constructor(age:number){this.age=age;}}classHumanextendsBasicLifeFormimplementsLifeform{move(){return"Walking";}}
当你抬头时Lifeform,你可以看到界面和它期望的一切,但是你很少会遇到意外实例化错误类的情况。
The moment you look up Lifeform, you can see the interface and everything it expects, but you seldom run into a situation where you instantiate the wrong class by accident.
既然已经说了什么时候不应该使用类和命名空间,那么什么时候应该使用它们呢?每当你需要同一个对象的多个实例时,内部状态对于对象的功能至关重要。
With everything said about when not to use classes and namespaces, when should you use them? Every time you need multiple instances of the same object, where the internal state is paramount to the functionality of the object.
传统的静态类在 TypeScript 中不存在,但 TypeScript 出于多种用途对类成员具有静态修饰符。
Traditional static classes don’t exist in TypeScript, but TypeScript has static modifiers for class members for several purposes.
静态类是无法实例化为具体对象的类。它们的目的是包含方法和其他成员,这些方法和其他成员存在一次,并且在从代码中的各个点访问时都是相同的。静态类对于仅以类作为抽象手段的编程语言(如 Java 或 C#)是必需的。在 JavaScript 以及随后的 TypeScript 中,有更多方式来表达自己。
Static classes are classes that can’t be instantiated into concrete objects. Their purpose is to contain methods and other members that exist once and are the same when being accessed from various points in your code. Static classes are necessary for programming languages that have only classes as their means of abstraction, like Java or C#. In JavaScript, and subsequently TypeScript, there are many more ways to express ourselves.
在 TypeScript 中,我们不能将类声明为static,但我们可以定义static类的成员。其行为正如您所期望的那样:方法或属性不是对象的一部分,但可以从类本身访问。
In TypeScript, we can’t declare classes to be static, but we can define static members on classes. The behavior is what you’d expect: the method or property is not part of an object but can be accessed from the class itself.
正如我们在11.5 节中看到的,只有静态成员的类是 TypeScript 中的反模式。函数是存在的;你可以为每个模块保存状态。通常的做法是将导出函数与模块范围的条目结合起来:
As we saw in Recipe 11.5, classes with only static members are an antipattern in TypeScript. Functions exist; you can keep state per module. A combination of exported functions and module-scoped entries is usually the way to go:
// Anti-PatternexportdefaultclassEnvironment{privatestaticvariableList:string[]=[]staticvariables():string[]{/* ... */}staticsetVariable(key:string,value:any):void{/* ... */}staticgetValue(key:string):unknown{/* ... */}}// Better: Module-scoped functions and variablesconstvariableList:string=[]exportfunctionvariables():string[]{/* ... */}exportfunctionsetVariable(key:string,value:any):void{/* ... */}exportfunctiongetValue(key:string):unknown{/* ... */}
// Anti-PatternexportdefaultclassEnvironment{privatestaticvariableList:string[]=[]staticvariables():string[]{/* ... */}staticsetVariable(key:string,value:any):void{/* ... */}staticgetValue(key:string):unknown{/* ... */}}// Better: Module-scoped functions and variablesconstvariableList:string=[]exportfunctionvariables():string[]{/* ... */}exportfunctionsetVariable(key:string,value:any):void{/* ... */}exportfunctiongetValue(key:string):unknown{/* ... */}
static但是类的某些部分仍然有用。我们在11.3 节中建立了类由静态成员和动态成员组成。
But there is still a use for static parts of a class. We established in Recipe 11.3 that a class consists of static members and dynamic members.
是constructor类的静态特性的一部分,而属性和方法是类的动态特性的一部分。使用static关键字我们可以添加这些静态特性。
The constructor is part of the static features of a class, and properties and methods are part of the dynamic features of a class. With the static keyword we can add to those static features.
让我们想象一个Point描述二维空间中点的类。它具有x和y坐标,我们创建一个方法来计算该点与另一个点之间的距离:
Let’s think of a class called Point that describes a point in a two-dimensional space. It has x and y coordinates, and we create a method that calculates the distance between this point and another one:
classPoint{x:number;y:number;constructor(x:number,y:number){this.x=x;this.y=y;}distanceTo(point:Point):number{constdx=this.x-point.x;constdy=this.y-point.y;returnMath.sqrt(dx*dx+dy*dy);}}consta=newPoint(0,0);constb=newPoint(1,5);constdistance=a.distanceTo(b);
classPoint{x:number;y:number;constructor(x:number,y:number){this.x=x;this.y=y;}distanceTo(point:Point):number{constdx=this.x-point.x;constdy=this.y-point.y;returnMath.sqrt(dx*dx+dy*dy);}}consta=newPoint(0,0);constb=newPoint(1,5);constdistance=a.distanceTo(b);
这是个好行为,但如果我们选择起点和终点,API 可能会感觉有点奇怪,尤其是因为无论哪个是第一个,距离都是相同的。静态方法 onPoint可以消除顺序,我们有一个很好的distance方法,它接受两个参数:
This is good behavior, but the API might feel a bit weird if we choose a starting point and end point, especially since the distance is the same no matter which one is first. A static method on Point gets rid of the order, and we have a nice distance method that takes two arguments:
classPoint{x:number;y:number;constructor(x:number,y:number){this.x=x;this.y=y;}distanceTo(point:Point):number{constdx=this.x-point.x;constdy=this.y-point.y;returnMath.sqrt(dx*dx+dy*dy);}staticdistance(p1:Point,p2:Point):number{returnp1.distanceTo(p2);}}consta=newPoint(0,0);constb=newPoint(1,5);constdistance=Point.distance(a,b);
classPoint{x:number;y:number;constructor(x:number,y:number){this.x=x;this.y=y;}distanceTo(point:Point):number{constdx=this.x-point.x;constdy=this.y-point.y;returnMath.sqrt(dx*dx+dy*dy);}staticdistance(p1:Point,p2:Point):number{returnp1.distanceTo(p2);}}consta=newPoint(0,0);constb=newPoint(1,5);constdistance=Point.distance(a,b);
使用在 JavaScript 中 ECMAScript 之前使用的构造函数/原型模式的类似版本如下所示:
A similar version using the constructor function/prototype pattern that was used pre-ECMAScript classes in JavaScript would look like this:
functionPoint(x,y){this.x=x;this.y=y;}Point.prototype.distanceTo=function(p){constdx=this.x-p.x;constdy=this.y-p.y;returnMath.sqrt(dx*dx+dy*dy);}Point.distance=function(a,b){returna.distanceTo(b);}
functionPoint(x,y){this.x=x;this.y=y;}Point.prototype.distanceTo=function(p){constdx=this.x-p.x;constdy=this.y-p.y;returnMath.sqrt(dx*dx+dy*dy);}Point.distance=function(a,b){returna.distanceTo(b);}
正如第 11.3 节中所述,我们可以很容易地看出哪些部分是静态的,哪些部分是动态的。原型中的所有内容都属于动态部分。其他所有内容都是静态的。
As in Recipe 11.3, we can easily see which parts are static and which parts are dynamic. Everything that is in the prototype belongs to the dynamic parts. Everything else is static.
但类不仅仅是构造函数/原型模式的语法糖。通过包含常规对象中不存在的私有字段,我们可以做一些实际上与类及其实例相关的事情。
But classes are not only syntactic sugar to the constructor function/prototype pattern. With the inclusion of private fields, which are absent in regular objects, we can do something that is actually related to classes and their instances.
例如,如果我们想要隐藏该distanceTo方法,因为它可能会造成混淆,并且我们希望用户使用静态方法,那么在前面加上一个简单的 private 修饰符distanceTo就可以让它从外部无法访问,但仍然可以从静态成员内部访问它:
If we want to, for example, hide the distanceTo method because it might be confusing and we’d prefer our users to use the static method instead, a simple private modifier in front of distanceTo makes it inaccessible from the outside but still keeps it accessible from within static members:
classPoint{x:number;y:number;constructor(x:number,y:number){this.x=x;this.y=y;}#distanceTo(point:Point):number{constdx=this.x-point.x;constdy=this.y-point.y;returnMath.sqrt(dx*dx+dy*dy);}staticdistance(p1:Point,p2:Point):number{returnp1.#distanceTo(p2);}}
classPoint{x:number;y:number;constructor(x:number,y:number){this.x=x;this.y=y;}#distanceTo(point:Point):number{constdx=this.x-point.x;constdy=this.y-point.y;returnMath.sqrt(dx*dx+dy*dy);}staticdistance(p1:Point,p2:Point):number{returnp1.#distanceTo(p2);}}
可见性也朝另一个方向发展。假设你有一个类代表Task系统中的某个任务,并且你想限制现有任务的数量。
The visibility also goes in the other direction. Let’s say you have a class that represents a certain Task in your system, and you want to limit the number of existing tasks.
我们使用一个静态私有字段nextId,从 开始0,随着每个构造实例 增加此私有字段Task。如果达到100,则抛出错误:
We use a static private field called nextId that we start at 0, and we increase this private field with every constructed instance Task. If we reach 100, we throw an error:
classTask{static#nextId=0;#id:number;constructor(){if(Task.#nextId>99){throw"Max number of tasks reached";}this.#id=Task.#nextId++;}}
classTask{static#nextId=0;#id:number;constructor(){if(Task.#nextId>99){throw"Max number of tasks reached";}this.#id=Task.#nextId++;}}
如果我们想通过来自后端的动态值来限制实例的数量,我们可以使用static实例化块来获取此数据并相应地更新静态私有字段:
If we want to limit the number of instances by a dynamic value from a backend, we can use a static instantiation block that fetches this data and updates the static private fields accordingly:
typeConfig={instances:number;};classTask{static#nextId=0;static#maxInstances:number;#id:number;static{fetch("/available-slots").then((res)=>res.json()).then((result:Config)=>{Task.#maxInstances=result.instances;});}constructor(){if(Task.#nextId>Task.#maxInstances){throw"Max number of tasks reached";}this.#id=Task.#nextId++;}}
typeConfig={instances:number;};classTask{static#nextId=0;static#maxInstances:number;#id:number;static{fetch("/available-slots").then((res)=>res.json()).then((result:Config)=>{Task.#maxInstances=result.instances;});}constructor(){if(Task.#nextId>Task.#maxInstances){throw"Max number of tasks reached";}this.#id=Task.#nextId++;}}
除了实例中的字段之外,在撰写本文时,TypeScript 不会检查静态字段是否已实例化。例如,如果我们从后端异步加载可用插槽数,则我们有一定的时间范围可以构造实例,但无法检查是否已达到最大值。
Other than fields in instances, TypeScript at the time of writing does not check if static fields are instantiated. If we, for example, load the number of available slots from a backend asynchronously, we have a certain time frame during which we can construct instances but have no check if we reached our maximum.
因此,即使 TypeScript 中没有静态类的构造,并且仅静态的类被视为反模式,在许多情况下静态成员可能仍有很好的用途。
So, even if there is no construct of a static class in TypeScript and static-only classes are considered an antipattern, there might be a good use for static members in many situations.
通过在tsconfigstrictPropertyInitialization中设置来激活严格属性初始化。true
Activate strict property initialization by setting strictPropertyInitialization to true in your tsconfig.
类可以看作是创建对象的代码模板。您可以定义属性和方法,并且只有通过实例化才能分配实际值。TypeScript 类采用基本的 JavaScript 类,并使用更多语法来增强它们以定义类型。例如,TypeScript 允许您以类型或接口的方式定义实例的属性:
Classes can be seen as code templates for creating objects. You define properties and methods, and only through instantiation do actual values get assigned. TypeScript classes take basic JavaScript classes and enhance them with more syntax to define types. For example, TypeScript allows you to define the properties of the instance in a type- or interface-like manner:
typeState="active"|"inactive";classAccount{id:number;userName:string;state:State;orders:number[];}
typeState="active"|"inactive";classAccount{id:number;userName:string;state:State;orders:number[];}
但是,这种表示法仅定义形状:它尚未设置任何具体值。当被转换为常规 JavaScript 时,所有这些属性都将被删除;它们仅存在于类型命名空间中。
However, this notation only defines the shape: it doesn’t set any concrete values, yet. When being transpiled to regular JavaScript, all those properties are erased; they exist only in the type namespace.
这种表示法可读性极高,可以让开发人员很好地了解预期的属性。但不能保证这些属性确实存在。如果我们不初始化它们,那么一切都会缺失或undefined。
This notation is arguably very readable and gives the developer a good idea of what properties to expect. But there is no guarantee that these properties actually exist. If we don’t initialize them, everything is either missing or undefined.
TypeScript 对此有保障。通过在tsconfig.json中strictPropertyInitialization设置该标志,TypeScript 将确保在从类创建新对象时实际初始化您期望的所有属性。true
TypeScript has safeguards for this. With the strictPropertyInitialization flag set to true in your tsconfig.json, TypeScript will make sure that all properties you’d expect are actually initialized when creating a new object from your class.
strictPropertyInitialization是 TypeScriptstrict模式的一部分。如果您在tsconfigstrict中设置为— 您应该这样做 — 您还会激活严格属性初始化。true
strictPropertyInitialization is part of TypeScript’s strict mode. If you set strict to true in your tsconfig—which you should—you also activate strict property initialization.
一旦激活,TypeScript 将用许多红色波浪线来欢迎你:
Once this is activated, TypeScript will greet you with many red squiggly lines:
classAccount{id:number;// ^ Property 'id' has no initializer and is// not definitely assigned in the constructor.(2564)userName:string;// ^ Property 'userName' has no initializer and is// not definitely assigned in the constructor.(2564)state:State;// ^ Property 'state' has no initializer and is// not definitely assigned in the constructor.(2564)orders:number[];// ^ Property 'orders' has no initializer and is// not definitely assigned in the constructor.(2564)}
classAccount{id:number;// ^ Property 'id' has no initializer and is// not definitely assigned in the constructor.(2564)userName:string;// ^ Property 'userName' has no initializer and is// not definitely assigned in the constructor.(2564)state:State;// ^ Property 'state' has no initializer and is// not definitely assigned in the constructor.(2564)orders:number[];// ^ Property 'orders' has no initializer and is// not definitely assigned in the constructor.(2564)}
太棒了!现在我们要确保每个属性都会收到一个值。有多种方法可以做到这一点。如果我们看一下示例Account,我们可以定义一些约束或规则(如果我们的应用程序域允许我们这样做):
Beautiful! Now it’s up to us to make sure that every property will receive a value. There are multiple ways to do this. If we look at the Account example, we can define some constraints or rules, if our application’s domain allows us to do so:
id并且userName需要进行设置;它们控制与我们的后端的通信并且对于显示是必需的。
id and userName need to be set; they control the communication to our backend and are necessary for display.
state也需要设置,但它的默认值是active。通常,我们软件中的账户都是活动的,除非有意设置为inactive。
state also needs to be set, but it has a default value of active. Usually, accounts in our software are active, unless they are set intentionally to inactive.
orders是一个包含订单 ID 的数组,但如果我们还没有订购任何东西怎么办?空数组同样有效,或者我们可以将其设置orders为尚未定义。
orders is an array that contains order IDs, but what if we haven’t ordered anything? An empty array works just as well, or maybe we set orders to not be defined yet.
有了这些限制,我们已经可以排除两个错误。我们默认设置state为active,并且我们将其设为orders可选。还有可能将 设置orders为 类型number[] | undefined,这与可选相同:
Given those constraints, we already can rule out two errors. We set state to be active by default, and we make orders optional. There’s also the possibility to set orders to be of type number[] | undefined, which is the same thing as optional:
classAccount{id:number;// still errorsuserName:string;// still errorsstate:State="active";// okorders?:number[];// ok}
classAccount{id:number;// still errorsuserName:string;// still errorsstate:State="active";// okorders?:number[];// ok}
其他两个属性仍然会抛出错误。通过添加constructor并初始化这些属性,我们也可以排除其他错误:
The other two properties still throw errors. By adding a constructor and initializing these properties, we rule out the other errors as well:
classAccount{id:number;userName:string;state:State="active";orders?:number[];constructor(userName:string,id:number){this.userName=userName;this.id=id;}}
classAccount{id:number;userName:string;state:State="active";orders?:number[];constructor(userName:string,id:number){this.userName=userName;this.id=id;}}
就是这样,一个合适的 TypeScript 类!TypeScript 还允许使用构造函数简写,您可以通过添加可见性修饰符(如 、 或 )将构造函数参数转换为具有相同名称和值的类属性public。private这protected是一个方便的功能,可以摆脱大量样板代码。重要的是不要在类形状中定义相同的属性:
That’s it, a proper TypeScript class! TypeScript also allows for a constructor shorthand, where you can turn constructor parameters into class properties with the same name and value by adding a visibility modifier like public, private, or protected. It’s a convenient feature that gets rid of a lot of boilerplate code. It’s important that you don’t define the same property in the class shape:
classAccount{state:State="active";orders?:number[];constructor(publicuserName:string,publicid:number){}}
classAccount{state:State="active";orders?:number[];constructor(publicuserName:string,publicid:number){}}
如果你现在看一下这个类,你会发现我们只依赖 TypeScript 特性。转译后的类(JavaScript 等效类)看起来有很大不同:
If you look at the class right now, you see that we rely only on TypeScript features. The transpiled class, the JavaScript equivalent, looks a lot different:
classAccount{constructor(userName,id){this.userName=userName;this.id=id;this.state="active";}}
classAccount{constructor(userName,id){this.userName=userName;this.id=id;this.state="active";}}
一切都在 中constructor,因为constructor定义了一个实例。
Everything is in the constructor, because the constructor defines an instance.
虽然 TypeScript 的快捷方式和类语法看起来不错,但要谨慎对待它们。TypeScript 近年来转变了方向,主要成为常规 JavaScript 之上类型的语法扩展,但它们存在多年的类功能仍然可用,并且为你的代码添加了与你预期不同的语义。如果你倾向于将代码变成“带有类型的 JavaScript”,那么在深入研究 TypeScript 类功能时要小心。
While TypeScript shortcuts and syntax for classes seem nice, be careful how much you buy into them. TypeScript switched gears in recent years to be mostly a syntax extension for types on top of regular JavaScript, but their class features that have existed for many years now are still available and add different semantics to your code than you’d expect. If you lean toward your code being “JavaScript with types,” be careful when you venture into the depths of TypeScript class features.
严格的属性初始化还可以理解复杂的场景,例如在通过 调用的函数内设置属性constructor。它还理解异步类可能会使您的类处于未初始化状态。
Strict property initialization also understands complex scenarios, like setting the property within a function that is being called via the constructor. It also understands that an async class might leave your class with a potentially uninitialized state.
假设您只想通过属性初始化您的类并从后端id获取。如果您在构造函数中执行异步调用并在调用完成后进行设置,您仍然会收到严格的属性初始化错误:userNameuserNamefetch
Let’s say you just want to initialize your class via an id property and fetch the userName from a backend. If you do the async call within your constructor and set userName after the fetch call is complete, you still get strict property initialization errors:
typeUser={id:number;userName:string;};classAccount{userName:string;// ^ Property 'userName' has no initializer and is// not definitely assigned in the constructor.(2564)state:State="active";orders?:number[];constructor(publicid:number){fetch(`/api/getName?id=${id}`).then((res)=>res.json()).then((data:User)=>(this.userName=data.userName??"not-found"));}}
typeUser={id:number;userName:string;};classAccount{userName:string;// ^ Property 'userName' has no initializer and is// not definitely assigned in the constructor.(2564)state:State="active";orders?:number[];constructor(publicid:number){fetch(`/api/getName?id=${id}`).then((res)=>res.json()).then((data:User)=>(this.userName=data.userName??"not-found"));}}
这是真的!没有任何信息告诉您调用fetch一定会成功,即使您catch出错并确保该属性将使用后备值进行初始化,在一定时间内您的对象仍会处于未初始化userName状态。
And it’s true! Nothing tells you that the fetch call will be successful, and even if you catch errors and make sure that the property will be initialized with a fallback value, there is a certain amount of time when your object has an uninitialized userName state.
你可以做一些事情来解决这个问题。一个不错的模式是使用一个异步工作的静态工厂函数,你首先获取数据,然后调用一个需要两个属性的构造函数:
You can do a few things to get around this. One nice pattern is having a static factory function that works asynchronously, where you get the data first and then call a constructor that expects both properties:
classAccount{state:State="active";orders?:number[];constructor(publicid:number,publicuserName:string){}staticasynccreate(id:number){constuser:User=awaitfetch(`/api/getName?id=${id}`).then((res)=>res.json());returnnewAccount(id,user.userName);}}
classAccount{state:State="active";orders?:number[];constructor(publicid:number,publicuserName:string){}staticasynccreate(id:number){constuser:User=awaitfetch(`/api/getName?id=${id}`).then((res)=>res.json());returnnewAccount(id,user.userName);}}
如果您有权访问这两个属性,则这允许在非异步上下文中实例化这两个对象,或者如果您只有id可用的属性,则允许在异步上下文中实例化这两个对象。我们切换职责并完全从构造函数中删除async。
This allows both objects to be instantiated in a non-async context if you have access to both properties, or within an async context if you have only id available. We switch responsibilities and remove async from the constructor entirely.
另一种技术是简单地忽略未初始化的状态。如果状态userName与您的应用程序完全无关,并且您只想在需要时访问它,该怎么办?使用明确赋值断言(感叹号)告诉 TypeScript 您将此属性视为已初始化:
Another technique is to simply ignore the uninitialized state. What if the state of userName is totally irrelevant to your application, and you want to access it only when needed? Use the definite assignment assertion (an exclamation mark) to tell TypeScript you will treat this property as initialized:
classAccount{userName!:string;state:State="active";orders?:number[];constructor(publicid:number){fetch(`/api/getName?id=${id}`).then((res)=>res.json()).then((data:User)=>(this.userName=data.userName));}}
classAccount{userName!:string;state:State="active";orders?:number[];constructor(publicid:number){fetch(`/api/getName?id=${id}`).then((res)=>res.json()).then((data:User)=>(this.userName=data.userName));}}
现在责任就在你的手中,用感叹号表示你有 TypeScript 特定的语法,你可以将其视为不安全的操作,包括运行时错误。
The responsibility is now in your hands, and with the exclamation mark you have TypeScript-specific syntax you can qualify as unsafe operation, runtime errors included.
您从基类扩展以重用功能,并且您的方法具有引用同一类的实例的签名。您想确保接口中没有其他子类混入,但您不想仅仅为了更改类型而覆盖方法。
You extend from base classes to reuse functionality, and your methods have signatures that refer to an instance of the same class. You want to make sure that no other subclasses are getting mixed in your interfaces, but you don’t want to override methods just to change the type.
使用this作为类型而不是实际的类类型。
Use this as type instead of the actual class type.
在这个例子中,我们想用类来模拟公告板软件的不同用户角色。我们从一个User由其用户 ID 标识并能够打开线程的通用类开始:
In this example, we want to model a bulletin board software’s different user roles using classes. We start with a general User class that is identified by its user ID and has the ability to open threads:
classUser{#id:number;static#nextThreadId:number;constructor(id:number){this.#id=id;}equals(user:User):boolean{returnthis.#id===user.#id;}asyncopenThread(title:string,content:string):Promise<number>{constthreadId=User.#nextThreadId++;awaitfetch("/createThread",{method:"POST",body:JSON.stringify({content,title,threadId,}),});returnthreadId;}}
classUser{#id:number;static#nextThreadId:number;constructor(id:number){this.#id=id;}equals(user:User):boolean{returnthis.#id===user.#id;}asyncopenThread(title:string,content:string):Promise<number>{constthreadId=User.#nextThreadId++;awaitfetch("/createThread",{method:"POST",body:JSON.stringify({content,title,threadId,}),});returnthreadId;}}
这个类还包含一个equals方法。在我们的代码库中,我们需要确保对用户的两个引用是相同的,并且由于我们通过用户 ID 来识别用户,因此我们可以轻松地比较数字。
This class also contains an equals method. Somewhere in our codebase, we need to make sure that two references to users are the same, and since we identify users by their ID, we can easily compare numbers.
User是所有用户的基类,因此如果我们添加具有更多权限的角色,我们可以轻松地从基User类继承。例如,Admin具有关闭线程的能力,并且它存储了一组我们可能在其他方法中使用的其他权限。
User is the base class of all users, so if we add roles with more privileges, we can easily inherit from the base User class. For example, Admin has the ability to close threads, and it stores a set of other privileges that we might use in other methods.
编程社区中存在很多争论,认为继承是一种最好忽略的技术,因为它的好处几乎无法抵消其弊端。尽管如此,JavaScript 的某些部分依赖于继承,例如 Web 组件。
There is much debate in the programming community if inheritance is a technique better to ignore since its benefits hardly outweigh its pitfalls. Nevertheless, some parts of JavaScript rely on inheritance, such as Web Components.
因为我们继承自User,所以我们不需要编写另一个openThread方法,并且我们可以重用相同的equals方法,因为所有管理员也是用户:
Since we inherit from User, we don’t need to write another openThread method, and we can reuse the same equals method since all administrators are also users:
classAdminextendsUser{#privileges:string[];constructor(id:number,privileges:string[]=[]){super(id);this.#privileges=privileges;}asynccloseThread(threadId:number){awaitfetch("/closeThread",{method:"POST",body:""+threadId,});}}
classAdminextendsUser{#privileges:string[];constructor(id:number,privileges:string[]=[]){super(id);this.#privileges=privileges;}asynccloseThread(threadId:number){awaitfetch("/closeThread",{method:"POST",body:""+threadId,});}}
设置好类之后,我们可以通过实例化正确的类来创建新类型的对象。我们还可以调用该User方法来比较两个用户是否相同:Adminequals
After setting up our classes, we can create new objects of type User and Admin by instantiating the right classes. We can also call the equals method to compare if two users might be the same:
constuser=newUser(1);constadmin=newAdmin(2);console.log(user.equals(admin));console.log(admin.equals(user));
constuser=newUser(1);constadmin=newAdmin(2);console.log(user.equals(admin));console.log(admin.equals(user));
不过,有一件事很麻烦:比较的方向。当然,比较两个数字是可交换的;如果我们将 a 与useran进行比较,这应该无关紧要admin,但如果我们考虑周围的类和子类型,则有一些改进的空间:
One thing is bothersome, though: the direction of comparison. Of course, comparing two numbers is commutative; it shouldn’t matter if we compare a user to an admin, but if we think about the surrounding classes and subtypes, there is some room for improvement:
检查 a 是否user等于 an是可以的admin,因为它可能会获得特权。
It’s OK to check if a user equals an admin, because it might gain privileges.
admin我们是否希望 an等于 a是值得怀疑的user,因为更广泛的超类型包含的信息较少。
It’s doubtful if we want an admin to equal a user, because the broader supertype has less information.
如果我们有另一个Moderator与相邻的子类Admin,我们肯定不希望比较它们,因为它们不共享基类之外的属性。
If we have another subclass of Moderator adjacent to Admin, we definitely don’t want to be able to compare them as they don’t share properties outside the base class.
尽管如此,按照现在开发的方式equals,所有比较都可以进行。我们可以通过更改要比较的类型来解决这个问题。我们User首先用注释来注释输入参数,但实际上我们想与同一类型的另一个实例进行比较。有一个类型可以做到这一点,它被称为this:
Still, in the way equals is developed now, all comparisons would work. We can work around this by changing the type of what we want to compare. We annotated the input parameter with User first, but in reality we want to compare with another instance of the same type. There is a type for that, and it is called this:
classUser{// ...equals(user:this):boolean{returnthis.#id===user.#id;}}
classUser{// ...equals(user:this):boolean{returnthis.#id===user.#id;}}
this这与我们在第 2.7 节中学习过的函数中的可擦除参数不同,因为参数类型允许我们在函数范围内this为全局变量设置具体类型。类型是对方法所在类的引用。它会根据实现而变化。因此,如果我们用in注释一个,它将变成继承自的类中的一个,或者一个,等等。这样,就需要与另一个类进行比较;否则,我们会得到一个错误:thisthisuserthisUserAdminUserModeratoradmin.equalsAdmin
This is different from the erasable this parameter we know from functions, which we learned about in Recipe 2.7, as the this parameter type allows us to set a concrete type for the this global variable within the scope of a function. The this type is a reference to the class where the method is located. And it changes depending on the implementation. So if we annotate a user with this in User, it becomes an Admin in the class that inherits from User, or a Moderator, and so on. With that, admin.equals expects another Admin class to be compared to; otherwise, we get an error:
console.log(admin.equals(user));// ^// Argument of type 'User' is not assignable to parameter of type 'Admin'.
console.log(admin.equals(user));// ^// Argument of type 'User' is not assignable to parameter of type 'Admin'.
反过来也行得通。由于Admin包含来自的所有属性User(毕竟它是一个子类),我们可以轻松比较user.equals(admin)。
The other way around still works. Since Admin contains all properties from User (it’s a subclass, after all), we can easily compare user.equals(admin).
this类型也可以用作返回类型。看一下这个OptionBuilder,它实现了构建器模式:
this types can also be used as return types. Take a look at this OptionBuilder, which implements the builder pattern:
classOptionBuilder<T=string|number|boolean>{#options:Map<string,T>=newMap();constructor(){}add(name:string,value:T):OptionBuilder<T>{this.#options.set(name,value);returnthis;}has(name:string){returnthis.#options.has(name);}build(){returnObject.fromEntries(this.#options);}}
classOptionBuilder<T=string|number|boolean>{#options:Map<string,T>=newMap();constructor(){}add(name:string,value:T):OptionBuilder<T>{this.#options.set(name,value);returnthis;}has(name:string){returnthis.#options.has(name);}build(){returnObject.fromEntries(this.#options);}}
它是 的软包装器Map,允许我们设置键/值对。它有一个可链接的接口,这意味着每次add调用后,我们都会返回当前实例,从而允许我们add一次又一次地调用add。请注意,我们用 注释了返回类型OptionBuilder<T>:
It’s a soft wrapper around a Map, which allows us to set key/value pairs. It has a chainable interface, which means that after each add call, we get the current instance back, allowing us to do add call after add call. Note that we annotated the return type with OptionBuilder<T>:
constoptions=newOptionBuilder().add("deflate",true).add("compressionFactor",10).build();
constoptions=newOptionBuilder().add("deflate",true).add("compressionFactor",10).build();
我们现在创建一个StringOptionBuilder继承自OptionBuilder并将可能元素的类型设置为 的string。我们还添加了一个safeAdd方法,用于检查某个值在写入之前是否已设置,这样我们就不会覆盖
以前的设置:
We are now creating a StringOptionBuilder that inherits from OptionBuilder and sets the type of possible elements to string. We also add a safeAdd method with checks if a certain value is already set before it is written, so we don’t override
previous settings:
classStringOptionBuilderextendsOptionBuilder<string>{safeAdd(name:string,value:string){if(!this.has(name)){this.add(name,value);}returnthis;}}
classStringOptionBuilderextendsOptionBuilder<string>{safeAdd(name:string,value:string){if(!this.has(name)){this.add(name,value);}returnthis;}}
当我们开始使用新的构建器时,我们发现如果第一步safeAdd有一个,我们就无法合理地使用:add
When we start using the new builder, we see that we can’t reasonably use safeAdd if we have an add as the first step:
constlanguages=newStringOptionBuilder().add("en","English").safeAdd("de","Deutsch")// ^// Property 'safeAdd' does not exist on type 'OptionBuilder<string>'.(2339).safeAdd("de","German").build();
constlanguages=newStringOptionBuilder().add("en","English").safeAdd("de","Deutsch")// ^// Property 'safeAdd' does not exist on type 'OptionBuilder<string>'.(2339).safeAdd("de","German").build();
TypeScript 告诉我们safeAdd类型 上不存在OptionBuilder<string>。 这个函数去哪儿了? 问题是add有一个非常宽泛的注释。 当然StringOptionBuilder是 的子类型OptionBuilder<string>,但使用注释,我们会丢失有关较窄类型的信息。 解决方案? 用作this返回类型:
TypeScript tells us that safeAdd does not exist on type OptionBuilder<string>. Where has this function gone? The problem is that add has a very broad annotation. Of course StringOptionBuilder is a subtype of OptionBuilder<string>, but with the annotation, we lose the information on the narrower type. The solution? Use this as return type:
classOptionBuilder<T=string|number|boolean>{// ...add(name:string,value:T):this{this.#options.set(name,value);returnthis;}}
classOptionBuilder<T=string|number|boolean>{// ...add(name:string,value:T):this{this.#options.set(name,value);returnthis;}}
与上一个示例的效果相同。在 中OptionBuilder<T>,this变为OptionBuilder<T>。在 中StringBuilder,this变为StringBuilder。如果您 returnthis并省略返回类型注释,this将成为推断的返回类型。因此,this显式使用取决于您的偏好(参见方案 2.1)。
The same effect happens as with the previous example. In OptionBuilder<T>, this becomes OptionBuilder<T>. In StringBuilder, this becomes StringBuilder. If you return this and leave out the return type annotation, this becomes the inferred return type. So using this explicitly depends on your preference (see Recipe 2.1).
编写一个类方法装饰器log来注释你的方法。
Write a class method decorator called log to annotate your methods.
装饰器设计模式在著名书籍《设计模式:可重用面向对象软件元素》(作者:Erich Gamma 等人,Addison-Wesley)中已有描述,描述了一种可以装饰类和方法以动态添加或覆盖某些行为的技术。
The decorator design pattern has been described in the renowned book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. (Addison-Wesley) and describes a technique that can decorate classes and methods to dynamically add or overwrite certain behavior.
装饰器最初是面向对象编程中自然产生的设计模式,如今已变得如此流行,以致具有面向对象特征的编程语言都添加了装饰器作为具有特殊语法的语言功能。您可以在 Java(称为注释)或 C#(称为属性)以及 JavaScript 中看到它的形式。
What began as a naturally emerging design pattern in object-oriented programming has become so popular that programming languages that feature object-oriented aspects have added decorators as a language feature with a special syntax. You can see forms of it in Java (called annotations) or C# (called attributes) and in JavaScript.
ECMAScript 关于装饰器的提案已经陷入提案地狱很长一段时间了,但在 2022 年进入了第 3 阶段(准备实施)。并且随着所有功能都达到第 3 阶段,TypeScript 是首批采用新规范的工具之一。
The ECMAScript proposal for decorators has been in proposal hell for quite a while but reached stage 3 (ready for implementation) in 2022. And with all features reaching stage 3, TypeScript is one of the first tools to pick up the new specification.
装饰器在 TypeScript 中存在了很长时间,并且处于experimentalDecorators编译器标记之下。在 TypeScript 5.0 中,原生 ECMAScript 装饰器提案已完全实现,无需标记即可使用。实际的 ECMAScript 实现与原始设计有根本区别,如果您在 TypeScript 5.0 之前开发了装饰器,它们将无法与新规范配合使用。请注意,打开标记experimentalDecorators 会关闭 ECMAScript 原生装饰器。此外,关于类型,lib.decorators.d.ts包含 ECMAScript 原生装饰器的所有类型信息,而lib.decorators.legacy.d.ts中的类型包含旧类型信息。请确保您的设置正确,并且不会使用来自错误
定义文件的类型。
Decorators have existed in TypeScript for a long time under the experimentalDecorators compiler flag. With TypeScript 5.0, the native ECMAScript decorator proposal is fully implemented and available without a flag. The actual ECMAScript implementation differs fundamentally from the original design, and if you developed decorators prior to TypeScript 5.0, they won’t work with the new specification. Note that a switched-on experimentalDecorators flag turns off the ECMAScript native decorators. Also, in regard to types, lib.decorators.d.ts contains all type information for the ECMAScript native decorators, while types in lib.decorators.legacy.d.ts contain old type information. Make sure your settings are correct and that you don’t consume types from the wrong
definition file.
装饰器允许我们装饰类中的几乎所有内容。对于此示例,我们想从方法装饰器开始,它允许我们记录方法调用的执行情况。
Decorators allow us to decorate almost anything in a class. For this example, we want to start with a method decorator that allows us to log the execution of method calls.
装饰器被描述为具有值和上下文的函数,两者都取决于您要装饰的类元素的类型。这些装饰器函数返回另一个函数,该函数将在您自己的方法之前执行(或在字段初始化之前,或在访问器调用之前等)。
Decorators are described as functions with a value and a context, both depending on the type of class element you want to decorate. Those decorator functions return another function that will be executed before your own method (or before field initialization, or before an accessor call, etc.).
方法的简单log装饰器可能看起来像这样:
A simple log decorator for methods could look like this:
functionlog(value:Function,context:ClassMethodDecoratorContext){returnfunction(this:any,...args:any[]){console.log(`calling${context.name.toString()}`);returnvalue.call(this,...args);};}classToggler{#toggled=false;@logtoggle(){this.#toggled=!this.#toggled;}}consttoggler=newToggler();toggler.toggle();
functionlog(value:Function,context:ClassMethodDecoratorContext){returnfunction(this:any,...args:any[]){console.log(`calling${context.name.toString()}`);returnvalue.call(this,...args);};}classToggler{#toggled=false;@logtoggle(){this.#toggled=!this.#toggled;}}consttoggler=newToggler();toggler.toggle();
该log函数遵循ClassMethodDecorator原始装饰器提案中定义的类型:
The log function follows a ClassMethodDecorator type defined in the original decorator proposal:
typeClassMethodDecorator=(value:Function,context:{kind:"method";name:string|symbol;access:{get():unknown};static:boolean;private:boolean;addInitializer(initializer:()=>void):void;})=>Function|void;
typeClassMethodDecorator=(value:Function,context:{kind:"method";name:string|symbol;access:{get():unknown};static:boolean;private:boolean;addInitializer(initializer:()=>void):void;})=>Function|void;
有许多装饰器上下文类型可用。lib.decorator.d.ts定义了以下装饰器:
Many decorator context types are available. lib.decorator.d.ts defines the following decorators:
typeClassMemberDecoratorContext=|ClassMethodDecoratorContext|ClassGetterDecoratorContext|ClassSetterDecoratorContext|ClassFieldDecoratorContext|ClassAccessorDecoratorContext;/*** The decorator context types provided to any decorator.*/typeDecoratorContext=|ClassDecoratorContext|ClassMemberDecoratorContext;
typeClassMemberDecoratorContext=|ClassMethodDecoratorContext|ClassGetterDecoratorContext|ClassSetterDecoratorContext|ClassFieldDecoratorContext|ClassAccessorDecoratorContext;/*** The decorator context types provided to any decorator.*/typeDecoratorContext=|ClassDecoratorContext|ClassMemberDecoratorContext;
您可以从名称中准确地读出他们针对的是某个类的哪个部分。
You can read from the names exactly which part of a class they target.
请注意,我们还没有编写详细的类型。我们求助于很多any,主要是因为类型可能变得非常复杂。如果我们想为所有参数添加类型,我们需要求助于很多泛型:
Note that we haven’t written detailed types yet. We resort to a lot of any, mostly because the types can get very complex. If we want to add types for all parameters, we need to resort to a lot of generics:
functionlog<This,Argsextendsany[],Return>(value:(this:This,...args:Args)=>Return,context:ClassMethodDecoratorContext):(this:This,...args:Args)=>Return{returnfunction(this:This,...args:Args){console.log(`calling${context.name.toString()}`);returnvalue.call(this,...args);};}
functionlog<This,Argsextendsany[],Return>(value:(this:This,...args:Args)=>Return,context:ClassMethodDecoratorContext):(this:This,...args:Args)=>Return{returnfunction(this:This,...args:Args){console.log(`calling${context.name.toString()}`);returnvalue.call(this,...args);};}
泛型类型参数对于描述我们传入的方法是必要的。我们想要捕获以下类型:
The generic type parameters are necessary to describe the method we are passing in. We want to catch the following types:
This是参数类型的泛型类型参数this(参见2.7 节)。我们需要设置this装饰器在对象实例的上下文中运行。
This is a generic type parameter for the this parameter type (see Recipe 2.7). We need to set this as decorators are run in the context of an object instance.
然后,我们将方法的参数表示为Args。正如我们在2.4 节中学到的,方法或函数的参数可以描述为一个元组。
Then we have the method’s arguments as Args. As we learned in Recipe 2.4, a method or function’s arguments can be described as a tuple.
最后,但并非最不重要的一点是Return类型参数。该方法需要返回特定类型的值,我们希望指定这一点。
Last, but not least, the Return type parameter. The method needs to return a value of a certain type, and we want to specify this.
有了这三种方法,我们就能以最通用的方式描述所有类的输入方法和输出方法。我们可以使用通用约束来确保我们的装饰器只在某些情况下起作用,但对于log,我们希望能够记录每个方法调用。
With all three, we are able to describe the input method as well as the output method in the most generic way, for all classes. We can use generic constraints to make sure that our decorator works only in certain cases, but for log, we want to be able to log every method call.
在撰写本文时,TypeScript 中的 ECMAScript 装饰器还比较新。类型会随着时间的推移而变得更好,因此您获取的类型信息可能已经好得多。
At the time of writing, ECMAScript decorators in TypeScript are fairly new. Types get better over time, so the type information you get may already be much better.
constructor我们还希望在调用该方法之前记录我们的类字段及其初始值:
We also want to log our class fields and their initial value before the constructor method is called:
classToggler{@logField#toggled=false;@logtoggle(){this.#toggled=!this.#toggled;}}
classToggler{@logField#toggled=false;@logtoggle(){this.#toggled=!this.#toggled;}}
为此,我们创建了另一个名为 的装饰器logField,它作用于ClassFieldDecoratorContext。装饰器提案对类字段的装饰器进行了如下描述:
For that, we create another decorator called logField, which works on a ClassFieldDecoratorContext. The decorator proposal describes the decorator for class fields as follows:
typeClassFieldDecorator=(value:undefined,context:{kind:"field";name:string|symbol;access:{get():unknown,set(value:unknown):void};static:boolean;private:boolean;})=>(initialValue:unknown)=>unknown|void;
typeClassFieldDecorator=(value:undefined,context:{kind:"field";name:string|symbol;access:{get():unknown,set(value:unknown):void};static:boolean;private:boolean;})=>(initialValue:unknown)=>unknown|void;
注意,值为。初始undefined值被传递给替换方法:
Note that the value is undefined. The initial value is being passed to the replacement method:
typeFieldDecoratorFn=(val:any)=>any;functionlogField<Val>(value:undefined,context:ClassFieldDecoratorContext):FieldDecoratorFn{returnfunction(initialValue:Val):Val{console.log(`Initializing${context.name.toString()}to${initialValue}`);returninitialValue;};}
typeFieldDecoratorFn=(val:any)=>any;functionlogField<Val>(value:undefined,context:ClassFieldDecoratorContext):FieldDecoratorFn{returnfunction(initialValue:Val):Val{console.log(`Initializing${context.name.toString()}to${initialValue}`);returninitialValue;};}
有一件事感觉不对劲。为什么我们需要为不同类型的成员使用不同的装饰器?我们的log装饰器难道不应该能够处理所有事情吗?我们的装饰器在特定的装饰器上下文中被调用,我们可以通过kind属性识别正确的上下文(我们在配方 3.2中看到的一种模式)。所以没有什么比编写一个log根据上下文执行不同装饰器调用的函数更容易的了,对吧?
There’s one thing that feels off. Why would we need different decorators for different kinds of members? Shouldn’t our log decorator be capable of handling it all? Our decorator is called in a specific decorator context, and we can identify the right context via the kind property (a pattern we saw in Recipe 3.2). So there’s nothing easier than writing a log function that does different decorator calls depending on the context, right?
嗯,既是也不是。当然,拥有一个正确分支的包装函数是可行的方法,但正如我们所见,类型定义非常复杂。如果不默认所有类型,找到一个可以处理所有类型的函数签名几乎是不可能的any。请记住:我们需要正确的函数签名类型;否则,装饰器将无法与类成员一起使用。
Well, yes and no. Of course, having a wrapper function that branches correctly is the way to go, but the type definitions, as we’ve seen, are pretty complex. Finding one function signature that can handle them all is close to impossible without defaulting to any everywhere. And remember: we need the right function signature typings; otherwise, the decorators won’t work with class members.
多个不同的函数签名只是尖叫函数重载。因此,我们不是为所有可能的装饰器找到一个函数签名,而是为字段装饰器、方法装饰器等创建重载。在这里,我们可以像键入单个装饰器一样键入它们。实现的函数签名接受any并将value所有必需的装饰器上下文类型合并在一起,因此我们可以随后进行适当的区分检查:
Multiple different function signatures just scream function overloads. So instead of finding one function signature for all possible decorators, we create overloads for field decorators, method decorators, and so on. Here, we can type them just as we would type the single decorators. The function signature for the implementation takes any for value and brings all required decorator context types in a union, so we can do proper discrimination checks afterward:
functionlog<This,Argsextendsany[],Return>(value:(this:This,...args:Args)=>Return,context:ClassMethodDecoratorContext):(this:This,...args:Args)=>Return;functionlog<Val>(value:Val,context:ClassFieldDecoratorContext):FieldDecoratorFn;functionlog(value:any,context:ClassMethodDecoratorContext|ClassFieldDecoratorContext){if(context.kind==="method"){returnlogMethod(value,context);}else{returnlogField(value,context);}}
functionlog<This,Argsextendsany[],Return>(value:(this:This,...args:Args)=>Return,context:ClassMethodDecoratorContext):(this:This,...args:Args)=>Return;functionlog<Val>(value:Val,context:ClassFieldDecoratorContext):FieldDecoratorFn;functionlog(value:any,context:ClassMethodDecoratorContext|ClassFieldDecoratorContext){if(context.kind==="method"){returnlogMethod(value,context);}else{returnlogField(value,context);}}
我们宁愿调用原始方法,而不是将所有实际代码都笨拙地放入if分支中。如果您不想暴露logMethod或logField函数,那么您可以将它们放在模块中并仅导出log。
Instead of fumbling all the actual code into the if branches, we’d rather call the original methods. If you don’t want to have your logMethod or logField functions exposed, then you can put them in a module and only export log.
装饰器类型有很多种,它们都有各种略有不同的字段。lib.decorators.d.ts中的类型定义非常出色,但如果您需要更多信息,请查看TC39 上的原始装饰器提案。它不仅包含有关所有类型装饰器的大量信息,还包含其他 TypeScript 类型来完善整个图景。
There are a lot of different decorator types, and they all have various fields that differ slightly. The type definitions in lib.decorators.d.ts are excellent, but if you need a bit more information, check out the original decorator proposal at TC39. Not only does it include extensive information on all types of decorators, but it also contains additional TypeScript typings that complete the picture.
我们最后要做的一件事是:适应在调用之前和之后logMethod进行记录。对于普通方法,这就像临时存储返回值一样简单:
There is one last thing we want to do: adapt logMethod to log both before and after the call. For normal methods, it’s as easy as temporarily storing the return value:
functionlog<This,Argsextendsany[],Return>(value:(this:This,...args:Args)=>Return,context:ClassMethodDecoratorContext){returnfunction(this:This,...args:Args){console.log(`calling${context.name.toString()}`);constval=value.call(this,...args);console.log(`called${context.name.toString()}:${val}`);returnval;};}
functionlog<This,Argsextendsany[],Return>(value:(this:This,...args:Args)=>Return,context:ClassMethodDecoratorContext){returnfunction(this:This,...args:Args){console.log(`calling${context.name.toString()}`);constval=value.call(this,...args);console.log(`called${context.name.toString()}:${val}`);returnval;};}
但对于异步方法,事情会变得更加有趣。调用异步方法会产生一个Promise。Promise本身可能已经执行,或者执行被推迟到以后。这意味着如果我们坚持之前的实现,被调用的日志消息可能会在方法实际产生值之前出现。
But for asynchronous methods, things get a little more interesting. Calling an asynchronous method yields a Promise. The Promise itself might already have been executed, or the execution is deferred to later. This means if we stick with the implementation from before, the called log message might appear before the method actually yields a value.
作为一种解决方法,我们需要将日志消息链接为产生结果后的下一步Promise。为此,我们需要检查该方法是否实际上是Promise。JavaScript Promises 很有趣,因为它们需要等待的只是有一个then方法。这是我们可以在辅助方法中检查的内容:
As a workaround, we need to chain the log message as the next step after the Promise yields a result. To do so, we need to check if the method is actually a Promise. JavaScript Promises are interesting because all they need to be awaited is having a then method. This is something we can check in a helper method:
functionisPromise(val:any):valisPromise<unknown>{return(typeofval==="object"&&val&&"then"inval&&typeofval.then==="function");}
functionisPromise(val:any):valisPromise<unknown>{return(typeofval==="object"&&val&&"then"inval&&typeofval.then==="function");}
这样,我们就可以决定是直接记录还是延迟记录,这取决于我们是否有Promise:
And with that, we decide whether to log directly or deferred based on if we have a Promise:
functionlogMethod<This,Argsextendsany[],Return>(value:(this:This,...args:Args)=>Return,context:ClassMethodDecoratorContext):(this:This,...args:Args)=>Return{returnfunction(this:This,...args:Args){console.log(`calling${context.name.toString()}`);constval=value.call(this,...args);if(isPromise(val)){val.then((p:unknown)=>{console.log(`called${context.name.toString()}:${p}`);returnp;});}else{console.log(`called${context.name.toString()}:${val}`);}returnval;};}
functionlogMethod<This,Argsextendsany[],Return>(value:(this:This,...args:Args)=>Return,context:ClassMethodDecoratorContext):(this:This,...args:Args)=>Return{returnfunction(this:This,...args:Args){console.log(`calling${context.name.toString()}`);constval=value.call(this,...args);if(isPromise(val)){val.then((p:unknown)=>{console.log(`called${context.name.toString()}:${p}`);returnp;});}else{console.log(`called${context.name.toString()}:${val}`);}returnval;};}
装饰器可能变得非常复杂,但最终是使 JavaScript 和 TypeScript 中的类更具表现力的有用工具。
Decorators can get very complex but are ultimately a useful tool to make classes in JavaScript and TypeScript more expressive.
到目前为止,所有食谱都涉及了 TypeScript 编程语言及其类型系统的特定方面。您已经学习了如何有效地使用第2章和第 3 章中的基本类型,在第 4 章中通过泛型提高代码的可重用性,并在第 5 章中使用条件类型、第 6 章中的字符串模板文字类型和第 7 章中的可变元组类型为非常微妙的情况制作高级类型。
All recipes up until now have dealt with specific aspects of the TypeScript programming language and its type system. You have learned about effectively using basic types in Chapters 2 and 3, making your code more reusable through generics in Chapter 4, and crafting advanced types for very delicate situations using conditional types in Chapter 5, string template literal types in Chapter 6, and variadic tuple types in Chapter 7.
我们在第 8 章中建立了辅助类型集合,并在第 9 章中解决了标准库的限制。我们在第 10 章中学习了如何使用 JSX 作为语言扩展,以及在第 11 章中学习了如何以及何时使用类。每个方法都详细讨论了每种方法的优缺点,为您提供了更好的工具来针对每种情况做出正确的决定,创建更好的类型、更强大的程序和稳定的开发流程。
We established a collection of helper types in Chapter 8 and worked around standard library limitations in Chapter 9. We learned how to work with JSX as a language extension in Chapter 10 and how and when to use classes in Chapter 11. Every recipe discussed in detail the pros and cons of each approach, giving you better tools to decide correctly for every situation, creating better types, more robust programs, and a stable development flow.
太多了!不过,还有一件事还缺,那就是把所有东西拼凑在一起的最后一块拼图:我们如何应对新型挑战?我们从哪里开始?我们需要注意什么?
That’s a lot! One thing is still missing, though, the final piece that brings everything together: how do we approach new type challenges? Where do we start? What do we need to look out for?
这些问题的答案构成了本章的内容。在这里,您将了解低维护类型的概念。我们将探索如何从简单类型开始,逐渐变得更加精致和强大的过程。您将了解TypeScript 游乐场的秘密功能以及如何处理使验证更容易的库。您将找到指南来帮助您做出艰难的决定,并了解最常见但难以克服的类型错误的解决方法,这些错误肯定会在您的 TypeScript 旅程中困扰您。
The answers to these questions make up the contents of this chapter. Here you will learn about the concept of low maintenance types. We will explore a process on how you can start with simple types first and gradually get more refined and stronger. You will learn about the secret features of the TypeScript playground and how to deal with libraries that make validation easier. You will find guides to help you make hard decisions and learn about workarounds to the most common yet tough-to-beat type errors that will definitely hit you in your TypeScript journey.
如果本书的其余部分将您从新手变成学徒,那么接下来的食谱将引导您成为专家。欢迎来到最后一章。
If the rest of the book brought you from novice to apprentice, the next recipes will lead you to become an expert. Welcome to the last chapter.
从其他类型中派生类型,从使用情况中推断,并创建低维护类型。
Derive types from others, infer from usage, and create low maintenance types.
在本书中,我们花了很多时间从其他类型创建类型。从已经存在的东西中派生出类型意味着我们花在编写和调整类型信息上的时间更少,而花在修复 JavaScript 中的错误和错误上的时间更多。
Throughout this book, we have spent a lot of time creating types from other types. The moment we can derive a type from something that already exists means we spend less time writing and adapting type information and more time fixing bugs and errors in JavaScript.
TypeScript 是 JavaScript 之上的一层元信息。我们的目标仍然是编写 JavaScript,但使其尽可能强大和简单:工具可帮助您保持高效,而不会妨碍您。
TypeScript is a layer of metainformation on top of JavaScript. Our goal is still to write JavaScript but make it as robust and easy as possible: tooling helps you stay productive and doesn’t get in your way.
这就是我通常编写 TypeScript 的方式:我编写常规 JavaScript,当 TypeScript 需要额外信息时,我很乐意添加一些额外的注释。但有一个条件:我不想费心维护类型。我宁愿创建可以在依赖项或环境发生变化时自我更新的类型。我将这种方法称为创建低维护类型。
That’s how I write TypeScript in general: I write regular JavaScript, and where TypeScript needs extra information, I happily add some extra annotations. One condition: I don’t want to be bothered maintaining types. I’d rather create types that can update themselves if their dependencies or surroundings change. I call this approach creating low maintenance types.
创建低维护类型分为三个部分:
Creating low maintenance types is a three-part process:
对您的数据进行建模或从现有模型推断。
Model your data or infer from existing models.
定义派生词(映射类型、部分等等)。
Define derivates (mapped types, partials, etc.).
用条件类型定义行为。
Define behavior with conditional types.
让我们看一下这个简短而不完整的copy函数。我想将文件从一个目录复制到另一个目录。为了让我的生活更轻松,我创建了一组默认选项,这样我就不必重复太多次:
Let’s take a look at this brief and incomplete copy function. I want to copy files from one directory to another. To make my life easier, I created a set of default options so I don’t have to repeat myself too much:
constdefaultOptions={from:"./src",to:"./dest",};functioncopy(options){// Let's merge default options and optionsconstallOptions={...defaultOptions,...options};// todo: Implementation of the rest}
constdefaultOptions={from:"./src",to:"./dest",};functioncopy(options){// Let's merge default options and optionsconstallOptions={...defaultOptions,...options};// todo: Implementation of the rest}
这是你在 JavaScript 中经常看到的模式。你立即看到的是 TypeScript 缺少一些类型信息。特别是函数options的参数。所以让我们为此添加一个类型!copyany
That’s a pattern you might see a lot in JavaScript. What you see immediately is that TypeScript misses some type information. Especially the options argument of the copy function is any at the moment. So let’s add a type for that!
我可以明确地创建类型:
I could create types explicitly:
typeOptions={from:string;to:string;};constdefaultOptions:Options={from:"./src",to:"./dest",};typePartialOptions={from?:string;to?:string;};functioncopy(options:PartialOptions){// Let's merge default options and optionsconstallOptions={...defaultOptions,...options};// todo: Implementation of the rest}
typeOptions={from:string;to:string;};constdefaultOptions:Options={from:"./src",to:"./dest",};typePartialOptions={from?:string;to?:string;};functioncopy(options:PartialOptions){// Let's merge default options and optionsconstallOptions={...defaultOptions,...options};// todo: Implementation of the rest}
这是一种合理的方法。您考虑类型,然后分配类型,然后获得所有您习惯的编辑器反馈和类型检查。但如果发生更改怎么办?假设我们向 添加了另一个字段Options;我们将不得不调整代码三次:
That’s a reasonable approach. You think about types, then you assign types, and then you get all the editor feedback and type-checking you are used to. But what if something changes? Let’s assume we add another field to Options; we would have to adapt our code three times:
typeOptions={from:string;to:string;overwrite:boolean;// added};constdefaultOptions:Options={from:"./src",to:"./dest",overwrite:true,// added};typePartialOptions={from?:string;to?:string;overwrite?:boolean;// added};
typeOptions={from:string;to:string;overwrite:boolean;// added};constdefaultOptions:Options={from:"./src",to:"./dest",overwrite:true,// added};typePartialOptions={from?:string;to?:string;overwrite?:boolean;// added};
但为什么呢?信息已经在那里了!在 中defaultOptions,我们告诉 TypeScript 我们正在寻找什么。让我们进行优化:
But why? The information is already there! In defaultOptions, we tell TypeScript exactly what we’re looking for. Let’s optimize:
删除该PartialOptions类型并使用实用程序类型Partial<T>可获得相同效果。您可能已经猜到了这一点。
Drop the PartialOptions type and use the utility type Partial<T> to get the same effect. You might have guessed this one already.
使用typeofTypeScript 中的运算符动态创建新类型:
Use the typeof operator in TypeScript to create a new type on the fly:
constdefaultOptions={from:"./src",to:"./dest",overwrite:true,};functioncopy(options:Partial<typeofdefaultOptions>){// Let's merge default options and optionsconstallOptions={...defaultOptions,...options};// todo: Implementation of the rest}
constdefaultOptions={from:"./src",to:"./dest",overwrite:true,};functioncopy(options:Partial<typeofdefaultOptions>){// Let's merge default options and optionsconstallOptions={...defaultOptions,...options};// todo: Implementation of the rest}
就这样。只需注释一下我们需要告诉 TypeScript 我们正在寻找什么:
There you go. Just annotate where we need to tell TypeScript what we’re looking for:
如果我们添加新字段,我们根本不必维护任何东西。
If we add new fields, we don’t have to maintain anything at all.
如果我们重命名一个字段,我们只会得到我们关心的信息:copy我们必须改变传递给函数的选项的所有用途。
If we rename a field, we get just the information we care about: all uses of copy where we have to change the options we pass to the function.
我们只有一个事实来源:实际defaultOptions对象。这是最重要的对象,因为它是运行时我们拥有的唯一信息。
We have one single source of truth: the actual defaultOptions object. This is the object that counts because it’s the only information we have at runtime.
我们的代码变得更加简洁。TypeScript 的侵入性更低,并且更符合我们编写 JavaScript 的方式。
And our code becomes a little bit more concise. TypeScript becomes less intrusive and more aligned to how we write JavaScript.
另一个例子是从一开始就伴随我们的:从方案 3.1开始的玩具店,在方案4.5和方案5.3中继续。重新审视这三个项目,思考如何只更改模型来 更新所有其他类型。
Another example is one that has accompanied us from the beginning: the toy shop that started in Recipe 3.1, and has continued in Recipes 4.5 and 5.3. Revisit all three items and think about how we can change only the model to get all other types updated.
逐步完善您的类型。从基本的原始类型和对象类型开始,然后子集化、添加泛型,最后全力以赴。本课中描述的过程将帮助您制作类型。这也是回顾所学知识的好方法。
Refine your types step by step. Start with basic primitive and object types, subset, add generics, and then go all-in advanced. The process described in this lesson will help you craft types. It’s also a good way to recap everything you’ve learned.
看一下下面的例子:
Take a look at the following example:
app.get("/api/users/:userID",function(req,res){if(req.method==="POST"){res.status(20).send({message:"Got you, user "+req.params.userId,});}});
app.get("/api/users/:userID",function(req,res){if(req.method==="POST"){res.status(20).send({message:"Got you, user "+req.params.userId,});}});
我们有一个Express 风格的服务器,它允许我们定义路线(或路径)并在请求 URL 时执行回调。
We have an Express-style server that allows us to define a route (or path) and executes a callback if the URL is requested.
回调接受两个参数:
The callback takes two arguments:
在这里,我们可以获得所使用的 HTTP 方法的信息— 例如GET,,,,— 以及其他传入的参数。在这个例子中,应该POST映射到一个包含用户标识符的参数!PUTDELETEuserIDuserID
Here we get information on the HTTP method used—for example, GET, POST, PUT, DELETE—and additional parameters that come in. In this example userID should be mapped to a parameter userID that, well, contains the user’s identifier!
这里我们要准备一个从服务器到客户端的正确响应。我们要发送正确的状态代码(方法status)并通过网络发送 JSON 输出。
Here we want to prepare a proper response from the server to the client. We want to send correct status codes (method status) and send JSON output over the wire.
我们在这个例子中看到的内容被大大简化了,但它很好地说明了我们要做什么。前面的例子也充满了错误!看一看:
What we see in this example is heavily simplified, but it gives a good idea of what we are up to. The previous example is also riddled with errors! Take a look:
app.get("/api/users/:userID",function(req,res){if(req.method==="POST"){/* Error 1 */res.status(20).send({/* Error 2 */message:"Welcome, user "+req.params.userId/* Error 3 */,});}});
app.get("/api/users/:userID",function(req,res){if(req.method==="POST"){/* Error 1 */res.status(20).send({/* Error 2 */message:"Welcome, user "+req.params.userId/* Error 3 */,});}});
三行实现代码,出现三个错误?发生了什么?
Three lines of implementation code and three errors? What happened?
第一个错误很微妙。虽然我们告诉应用程序我们想要监听GET请求(因此app.get),但只有当请求方法是时我们才会执行某些操作POST。在我们的应用程序的这个特定点,req.method不能是POST。所以我们永远不会发送任何响应,这可能会导致意外超时。
The first error is nuanced. While we tell our app that we want to listen to GET requests (hence app.get), we do something only if the request method is POST. At this particular point in our application, req.method can’t be POST. So we would never send any response, which might lead to unexpected timeouts.
我们明确发送状态代码真是太好了!20但这不是有效的状态代码。客户端可能不明白这里发生了什么。
It’s great that we explicitly send a status code! 20 isn’t a valid status code, though. Clients might not understand what’s happening here.
这是我们想要返回的响应。我们访问了解析后的参数,但有一个拼写错误。它是userID,而不是userId。我们所有的用户都会收到“欢迎,用户未定义!”的欢迎信息。你肯定在外面见过这种东西!
This is the response we want to send back. We access the parsed arguments but have a typo. It’s userID, not userId. All our users would be greeted with “Welcome, user undefined!” Something you definitely have seen in the wild!
解决此类问题是 TypeScript 的主要目的。TypeScript 希望比您更了解您的 JavaScript 代码。当 TypeScript 无法理解您的意思时,您可以通过提供额外的类型信息来提供帮助。问题是,添加类型通常很难开始。您可能在脑海中想到了最令人费解的极端情况,但不知道如何找到它们。
Solving issues like this is TypeScript’s main purpose. TypeScript wants to understand your JavaScript code better than you do. And where TypeScript can’t figure out what you mean, you can assist by providing extra type information. The problem is that it’s often hard to start adding types. You might have the most puzzling edge cases in your mind but don’t know how to get to them.
我想提出一个流程,它既可以帮助你入门,又可以告诉你什么时候该停下来。你可以一步步提高类型的强度。每次改进都会变得更好,你可以在更长的时间内提高类型安全性。让我们开始吧!
I want to propose a process that may help you get started and also shows you where there’s a good place to stop. You can increase the strengths of your types step by step. It gets better with each refinement, and you can increase type safety over a longer period of time. Let’s start!
我们从一些基本类型信息开始。我们有一个app指向get函数的对象。该get函数接受path一个字符串和一个回调:
We start with some basic type information. We have an app object that points to a get function. The get function takes a path, which is a string, and a callback:
constapp={get/* post, put, delete, ... to come! */,};functionget(path:string,callback:CallbackFn){// to be implemented --> not important right now}
constapp={get/* post, put, delete, ... to come! */,};functionget(path:string,callback:CallbackFn){// to be implemented --> not important right now}
CallbackFn是一个返回并接受两个参数的函数类型void:
CallbackFn is a function type that returns void and takes two arguments:
req,其类型为ServerRequest
req, which is of type ServerRequest
reply,其类型为ServerReply
reply, which is of type ServerReply
typeCallbackFn=(req:ServerRequest,reply:ServerReply)=>void;
typeCallbackFn=(req:ServerRequest,reply:ServerReply)=>void;
ServerRequest在大多数框架中,这是一个相当复杂的对象。我们做了一个简化版本以作演示。我们传入一个method字符串,例如"GET",,,,等等。它还有一个记录。记录是将一组键与一组属性相关联的对象
。现在,我们希望允许每个键映射到一个属性。我们"POST"稍后会重构这个:"PUT""DELETE"paramsstringstring
ServerRequest is a pretty complex object in most frameworks. We do a simplified version for demonstration purposes. We pass in a method string, for "GET", "POST", "PUT", "DELETE", and so on. It also has a params record. Records are objects that
associate a set of keys with a set of properties. For now, we want to allow every string key to be mapped to a string property. We’ll refactor this one later:
typeServerRequest={method:string;params:Record<string,string>;};
typeServerRequest={method:string;params:Record<string,string>;};
对于ServerReply,我们列出了一些函数,因为我们知道实际ServerReply对象有更多函数。send函数带有一个可选参数,obj其中包含我们要发送的数据。我们可以status使用流畅的接口通过该函数设置状态代码:1
For ServerReply, we lay out some functions, knowing that a real ServerReply object has many more. A send function takes an optional argument obj with the data we want to send. We have the possibility to set a status code with the status function using a fluent interface:1
typeServerReply={send:(obj?:any)=>void;status:(statusCode:number)=>ServerReply;};
typeServerReply={send:(obj?:any)=>void;status:(statusCode:number)=>ServerReply;};
通过一些非常基本的复合类型和简单的路径原始类型,我们已经为项目添加了很多类型安全性。我们可以排除几个错误:
With some very basic compound types and a simple primitive type for paths, we already added a lot of type safety to our project. We can rule out a couple of errors:
app.get("/api/users/:userID",function(req,res){if(req.method===2){// ^ This condition will always return 'false' since the types// 'string' and 'number' have no overlap.(2367)res.status("200").send()// ^// Argument of type 'string' is not assignable to// parameter of type 'number'.(2345)}});
app.get("/api/users/:userID",function(req,res){if(req.method===2){// ^ This condition will always return 'false' since the types// 'string' and 'number' have no overlap.(2367)res.status("200").send()// ^// Argument of type 'string' is not assignable to// parameter of type 'number'.(2345)}});
这很棒,但还有很多事情要做。我们仍然可能发送错误的状态代码(任何数字都可能),并且不知道可能的 HTTP 方法(任何字符串都可能)。所以让我们改进我们的类型。
That’s great, but there’s still a lot to do. We can still send wrong status codes (any number is possible) and have no clue about the possible HTTP methods (any string is possible). So let’s refine our types.
您可以将原始类型视为该特定类别的所有可能值的集合。例如,string包括所有可以用 JavaScript 表达的字符串,
number包括所有可能具有双精度浮点数的数字,并boolean包括所有可能的布尔值,即true和false。
You can see primitive types as a set of all possible values of that certain category. For example, string includes all possible strings that can be expressed in JavaScript,
number includes all possible numbers with double float precision, and boolean includes all possible Boolean values, which are true and false.
TypeScript 允许你将那些集合细化为更小的子集。例如,我们可以创建一个类型Methods,其中包含我们可以接收的
HTTP 方法的所有可能的字符串:
TypeScript allows you to refine those sets to smaller subsets. For example, we can create a type Methods that includes all possible strings we can receive for
HTTP methods:
typeMethods="GET"|"POST"|"PUT"|"DELETE";typeServerRequest={method:Methods;params:Record<string,string>;};
typeMethods="GET"|"POST"|"PUT"|"DELETE";typeServerRequest={method:Methods;params:Record<string,string>;};
Methods是较大集合的较小集合string。Methods也是文字类型的联合类型,是给定集合的最小单位。文字字符串。文字数字。没有歧义:它只是"GET"。您可以将它们与其他文字类型放在联合中,从而创建任何较大类型的子集。您还可以使用和的文字类型string或number不同的复合对象类型来创建一个子集。有很多可能性可以将文字类型组合并放入联合中。
Methods is a smaller set of the bigger string set. Methods is also a union type of literal types, the smallest unit of a given set. A literal string. A literal number. There is no ambiguity: it’s just "GET". You put them in a union with other literal types, creating a subset of whatever bigger types you have. You can also do a subset with literal types of both string and number, or different compound object types. There are lots of possibilities to combine and put literal types into unions.
这对我们的服务器回调有直接影响。突然间,我们可以区分这四种方法(如果需要,可以更多),并且可以穷尽代码中的所有可能性。TypeScript 将指导我们。
This has an immediate effect on our server callback. Suddenly, we can differentiate between those four methods (or more if necessary) and can exhaust all possibilities in code. TypeScript will guide us.
这样就少了一个错误类别。我们现在确切地知道了哪些可能的 HTTP 方法可用。我们可以对 HTTP 状态代码执行相同的操作,方法是定义一个statusCode可以采用的有效数字子集:
That’s one less category of errors. We now know exactly which possible HTTP methods are available. We can do the same for HTTP status codes, by defining a subset of valid numbers that statusCode can take:
typeStatusCode=100|101|102|200|201|202|203|204|205|206|207|208|226|300|301|302|303|304|305|306|307|308|400|401|402|403|404|405|406|407|408|409|410|411|412|413|414|415|416|417|418|420|422|423|424|425|426|428|429|431|444|449|450|451|499|500|501|502|503|504|505|506|507|508|509|510|511|598|599;typeServerReply={send:(obj?:any)=>void;status:(statusCode:StatusCode)=>ServerReply;};
typeStatusCode=100|101|102|200|201|202|203|204|205|206|207|208|226|300|301|302|303|304|305|306|307|308|400|401|402|403|404|405|406|407|408|409|410|411|412|413|414|415|416|417|418|420|422|423|424|425|426|428|429|431|444|449|450|451|499|500|501|502|503|504|505|506|507|508|509|510|511|598|599;typeServerReply={send:(obj?:any)=>void;status:(statusCode:StatusCode)=>ServerReply;};
类型StatusCode再次是联合类型。这样,我们就可以排除另一类错误。突然,这样的代码失败了:
Type StatusCode is again a union type. And with that, we exclude another category of errors. Suddenly, code like that fails:
app.get("/api/user/:userID",(req,res)=>{if(req.method==="POS"){// ^ This condition will always return 'false' since// the types 'Methods' and '"POS"' have no overlap.(2367)res.status(20)// ^// Argument of type '20' is not assignable to parameter of// type 'StatusCode'.(2345)}})
app.get("/api/user/:userID",(req,res)=>{if(req.method==="POS"){// ^ This condition will always return 'false' since// the types 'Methods' and '"POS"' have no overlap.(2367)res.status(20)// ^// Argument of type '20' is not assignable to parameter of// type 'StatusCode'.(2345)}})
当我们用 定义路由时app.get,我们隐式地知道唯一可能的 HTTP 方法是"GET"。但是对于我们的类型定义,我们仍然必须检查联合的所有可能部分。
When we define a route with app.get, we implicitly know that the only HTTP method possible is "GET". But with our type definitions, we still have to check for all possible parts of the union.
的类型CallbackFn是正确的,因为我们可以为所有可能的 HTTP 方法定义回调函数,但是如果我们明确调用app.get,那么最好节省一些额外的步骤,这些步骤只是为了符合类型所必需的。
The type for CallbackFn is correct, as we could define callback functions for all possible HTTP methods, but if we explicitly call app.get, it would be nice to save some extra steps, which are only necessary to comply with typings.
TypeScript 泛型可以提供帮助。我们希望ServerRequest以一种可以指定部分Methods而不是整个集合的方式进行定义。为此,我们使用泛型语法,我们可以像定义函数一样定义参数:
TypeScript generics can help. We want to define ServerRequest in a way that we can specify a part of Methods instead of the entire set. For that, we use the generic syntax where we can define parameters as we would do with functions:
typeServerRequest<MetextendsMethods>={method:Met;params:Record<string,string>;};
typeServerRequest<MetextendsMethods>={method:Met;params:Record<string,string>;};
事情的经过如下:
Here is what happens:
ServerRequest变成通用类型,如尖括号所示。
ServerRequest becomes a generic type, as indicated by the angle brackets.
我们定义了一个名为的通用参数Met,它是类型的子集Methods。
We define a generic parameter called Met, which is a subset of type Methods.
我们使用这个泛型参数作为泛型变量来定义方法。
We use this generic parameter as a generic variable to define the method.
通过这种改变,我们可以指定不同的ServerRequest变体而无需
重复:
With that change, we can specify different ServerRequest variants without
duplicating:
typeOnlyGET=ServerRequest<"GET">;typeOnlyPOST=ServerRequest<"POST">;typePOSTorPUT=ServerRquest<"POST"|"PUT">;
typeOnlyGET=ServerRequest<"GET">;typeOnlyPOST=ServerRequest<"POST">;typePOSTorPUT=ServerRquest<"POST"|"PUT">;
由于我们改变了的接口ServerRequest,我们必须改变所有使用的其他类型ServerRequest,如CallbackFn和get函数:
Since we changed the interface of ServerRequest, we have to change all our other types that use ServerRequest, like CallbackFn and the get function:
typeCallbackFn<MetextendsMethods>=(req:ServerRequest<Met>,reply:ServerReply)=>void;functionget(path:string,callback:CallbackFn<"GET">){// to be implemented}
typeCallbackFn<MetextendsMethods>=(req:ServerRequest<Met>,reply:ServerReply)=>void;functionget(path:string,callback:CallbackFn<"GET">){// to be implemented}
通过该get函数,我们将一个实际参数传递给我们的泛型类型。我们知道这不仅仅是的子集;我们确切地知道我们正在处理Methods哪个子集
。
With the get function, we pass an actual argument to our generic type. We know that this won’t be just a subset of Methods; we know exactly which subset we are
dealing with.
现在,当我们使用时app.get,我们只有一个可能的值req.method:
Now, when we use app.get, we only have one possible value for req.method:
app.get("/api/users/:userID",function(req,res){req.method;// can only be GET});
app.get("/api/users/:userID",function(req,res){req.method;// can only be GET});
这确保我们在创建回调时不会假设 HTTP 方法(例如"POST"或类似)可用app.get。我们此时确切地知道我们在处理什么,因此让我们在我们的类型中反映这一点。
This ensures we don’t assume HTTP methods like "POST" or similar are available when we create an app.get callback. We know exactly what we are dealing with at this point, so let’s reflect that in our types.
我们已经做了很多工作来确保其request.method类型合理并代表实际情况。对联合类型进行子集化的一个好处是,我们可以在其之外创建一个类型安全Methods的通用回调函数:app.get
We already did a lot to make sure that request.method is reasonably typed and represents the actual state of affairs. One nice benefit of subsetting the Methods union type is that we can create a general-purpose callback function outside of app.get that is type safe:
consthandler:CallbackFn<"PUT"|"POST">=function(res,req){res.method// can be "POST" or "PUT"};consthandlerForAllMethods:CallbackFn<Methods>=function(res,req){res.method// can be all methods};app.get("/api",handler);// ^// Argument of type 'CallbackFn<"POST" | "PUT">' is not// assignable to parameter of type 'CallbackFn<"GET">'.app.get("/api",handlerForAllMethods);// This works
consthandler:CallbackFn<"PUT"|"POST">=function(res,req){res.method// can be "POST" or "PUT"};consthandlerForAllMethods:CallbackFn<Methods>=function(res,req){res.method// can be all methods};app.get("/api",handler);// ^// Argument of type 'CallbackFn<"POST" | "PUT">' is not// assignable to parameter of type 'CallbackFn<"GET">'.app.get("/api",handlerForAllMethods);// This works
我们还没有触及的是输入params对象。到目前为止,我们得到了一条允许访问每个string键的记录。现在我们的任务是让它更具体一点!
What we haven’t touched yet is typing the params object. So far, we get a record that allows accessing every string key. It’s our task now to make that a little more specific!
我们通过添加另一个通用变量来实现这一点,一个用于方法,一个用于我们的可能键Record:
We do so by adding another generic variable, one for methods and one for the possible keys in our Record:
typeServerRequest<MetextendsMethods,Parextendsstring=string>={method:Met;params:Record<Par,string>;};
typeServerRequest<MetextendsMethods,Parextendsstring=string>={method:Met;params:Record<Par,string>;};
泛型类型变量Par可以是类型的子集string,默认值是每个字符串。这样,我们就可以知道ServerRequest我们期望哪些键:
The generic type variable Par can be a subset of type string, and the default value is every string. With that, we can tell ServerRequest which keys we expect:
// request.method = "GET"// request.params = {// userID: string// }typeWithUserID=ServerRequest<"GET","userID">;
// request.method = "GET"// request.params = {// userID: string// }typeWithUserID=ServerRequest<"GET","userID">;
get让我们向我们的函数和类型添加新的参数CallbackFn,以便我们可以设置请求的参数:
Let’s add the new argument to our get function and the CallbackFn type, so we can set the requested parameters:
functionget<Parextendsstring=string>(path:string,callback:CallbackFn<"GET",Par>){// to be implemented}constapp={get/* post, put, delete, ... to come! */,};typeCallbackFn<MetextendsMethods,Parextendsstring>=(req:ServerRequest<Met,Par>,reply:ServerReply)=>void;
functionget<Parextendsstring=string>(path:string,callback:CallbackFn<"GET",Par>){// to be implemented}constapp={get/* post, put, delete, ... to come! */,};typeCallbackFn<MetextendsMethods,Parextendsstring>=(req:ServerRequest<Met,Par>,reply:ServerReply)=>void;
如果我们没有Par明确设置,则类型将按照我们习惯的方式工作,因为Par默认为string。但是,如果我们设置它,我们就会突然对req.params对象有一个适当的定义:
If we don’t set Par explicitly, the type works like we are accustomed to, since Par defaults to string. If we set it, though, we suddenly have a proper definition for the req.params object:
app.get<"userID">("/api/users/:userID",function(req,res){req.params.userID;// Works!!req.params.anythingElse;// doesn't work!!});
app.get<"userID">("/api/users/:userID",function(req,res){req.params.userID;// Works!!req.params.anythingElse;// doesn't work!!});
太棒了!不过,还有一点可以改进。我们仍然可以将每个字符串传递给path的参数。如果我们也可以在其中app.get进行反射,那不是更好吗?我们可以!这就是字符串模板文字类型(参见第 6 章)发挥作用的地方。Par
That’s great! One little thing can be improved, though. We still can pass every string to the path argument of app.get. Wouldn’t it be better if we could reflect Par in there as well? We can! This is where string template literal types (see Chapter 6) come into play.
让我们创建一个名为的类型,IncludesRouteParams以确保Par以 Express 风格的方式在参数名称前添加冒号:
Let’s create a type called IncludesRouteParams to make sure that Par is properly included in the Express-style way of adding a colon in front of the parameter name:
typeIncludesRouteParams<Parextendsstring>=|`${string}/:${Par}`|`${string}/:${Par}/${string}`;
typeIncludesRouteParams<Parextendsstring>=|`${string}/:${Par}`|`${string}/:${Par}/${string}`;
泛型类型IncludesRouteParams接受一个参数,它是的子集string。它创建两个模板文字的联合类型:
The generic type IncludesRouteParams takes one argument, which is a subset of string. It creates a union type of two template literals:
第一个模板字面量以any string开头,然后包含一个/字符,后面跟着一个:字符,最后是参数名称。这确保我们能够捕获参数位于路由字符串末尾的所有情况。
The first template literal starts with any string, then includes a / character followed by a : character, followed by the parameter name. This ensures that we catch all cases where the parameter is at the end of the route string.
第二个模板字面量以any string开头,后跟相同模式的/、:和参数名称。然后是另一个/字符,后跟任何字符串。联合类型的这个分支确保我们捕获参数位于路由内某处的所有情况。
The second template literal starts with any string, followed by the same pattern of /, :, and the parameter name. Then we have another / character, followed by any string. This branch of the union type makes sure we catch all cases where the parameter is somewhere within a route.
IncludesRouteParams参数名称userID在不同测试用例中的行为如下:
This is how IncludesRouteParams with the parameter name userID behaves with different test cases:
consta:IncludesRouteParams<"userID">="/api/user/:userID";// worksconstb:IncludesRouteParams<"userID">="/api/user/:userID/orders";// worksconstc:IncludesRouteParams<"userID">="/api/user/:userId";// breaksconstd:IncludesRouteParams<"userID">="/api/user";// breaksconste:IncludesRouteParams<"userID">="/api/user/:userIDAndmore";// breaks
consta:IncludesRouteParams<"userID">="/api/user/:userID";// worksconstb:IncludesRouteParams<"userID">="/api/user/:userID/orders";// worksconstc:IncludesRouteParams<"userID">="/api/user/:userId";// breaksconstd:IncludesRouteParams<"userID">="/api/user";// breaksconste:IncludesRouteParams<"userID">="/api/user/:userIDAndmore";// breaks
让我们在get函数声明中包含新的实用程序类型:
Let’s include our new utility type in the get function declaration:
functionget<Parextendsstring=string>(path:IncludesRouteParams<Par>,callback:CallbackFn<"GET",Par>){// to be implemented}app.get<"userID">("/api/users/:userID",function(req,res){req.params.userID;// Yes!});
functionget<Parextendsstring=string>(path:IncludesRouteParams<Par>,callback:CallbackFn<"GET",Par>){// to be implemented}app.get<"userID">("/api/users/:userID",function(req,res){req.params.userID;// Yes!});
太棒了!我们得到了另一个安全机制,以确保我们不会错过将参数添加到实际路线。这很强大。
Great! We get another safety mechanism to ensure that we don’t miss out on adding the parameters to the actual route. That’s powerful.
但你猜怎么着:我仍然不满意。当你的路线变得稍微复杂一些时,这种方法的一些问题就会显现出来:
But guess what: I’m still not happy with it. A few issues with that approach become apparent the moment your routes get a little more complex:
第一个问题是我们需要在泛型类型参数中明确说明我们的参数。我们必须绑定Par到"userID",即使我们无论如何都会在path函数的参数中指定它。这不是 JavaScript-y!
The first issue is that we need to explicitly state our parameters in the generic type parameter. We have to bind Par to "userID", even though we would specify it anyway in the path argument of the function. This is not JavaScript-y!
这种方法仅处理一个路由参数。例如,当我们添加一个联合时,只要其中一个"userID" | "orderId"参数可用,故障安全检查就会得到满足。这就是集合的工作方式。它可以是其中一个,也可以是另一个。
This approach handles only one route parameter. The moment we add a union—for example, "userID" | "orderId"—the fail-safe check is satisfied with only one of those arguments being available. That’s how sets work. It can be one or the other.
一定有更好的方法。而且确实有。否则,这个食谱的结局会非常糟糕。
There must be a better way. And there is. Otherwise, this recipe would end on a very bitter note.
让我们反转顺序!我们不是在泛型类型变量中定义路由参数,而是path从传递的第一个参数中提取变量app.get:
Let’s inverse the order! Instead of defining the route params in a generic type variable, we extract the variables from the path passed as the first argument of app.get:
functionget<Pathextendsstring=string>(path:Path,callback:CallbackFn<"GET",ParseRouteParams<Path>>){// to be implemented}
functionget<Pathextendsstring=string>(path:Path,callback:CallbackFn<"GET",ParseRouteParams<Path>>){// to be implemented}
我们删除Par泛型类型并添加Path,它可以是任何的子集string。当我们设置path为这个泛型类型时Path,我们将参数传递给时get,我们会捕获其字符串文字类型。我们传递给一个我们尚未创建的Path新泛型类型。ParseRouteParams
We remove the Par generic type and add Path, which can be a subset of any string. When we set path to this generic type Path, the moment we pass a parameter to get, we catch its string literal type. We pass Path to a new generic type ParseRouteParams that we haven’t created yet.
让我们开始吧ParseRouteParams。在这里,我们再次切换事件的顺序。我们不再将请求的路由参数传递给泛型以确保路径正确,而是传递路由路径并提取可能的路由参数。为此,我们需要创建一个条件类型。
Let’s work on ParseRouteParams. Here, we switch the order of events again. Instead of passing the requested route params to the generic to make sure the path is all right, we pass the route path and extract the possible route params. For that, we need to create a conditional type.
条件类型在语法上类似于 JavaScript 中的三元运算符。检查条件,如果条件满足,则返回分支 A;否则,返回分支 B。例如:
Conditional types are syntactically similar to the ternary operator in JavaScript. You check for a condition, and if the condition is met, you return branch A; otherwise, you return branch B. For example:
typeParseRouteParams<Route>=Routeextends`${string}/:${inferP}`?P:never;
typeParseRouteParams<Route>=Routeextends`${string}/:${inferP}`?P:never;
在这里,我们检查是否Route是每个以 Express 样式结尾的参数结尾的路径的子集(前面带有"/:")。如果是,我们推断此字符串,这意味着我们将其内容捕获到一个新变量中。如果满足条件,我们将返回新提取的字符串;否则,我们返回never,例如:“没有路由参数。”
Here, we check if Route is a subset of every path that ends with the parameter at the end Express-style (with a preceding "/:"). If so, we infer this string, which means we capture its contents into a new variable. If the condition is met, we return the newly extracted string; otherwise, we return never, as in: “there are no route parameters.”
如果我们尝试一下,我们会得到类似这样的结果:
If we try it, we get something like:
typeParams=ParseRouteParams<"/api/user/:userID">;// Params is "userID"typeNoParams=ParseRouteParams<"/api/user">;// NoParams is never: no params!
typeParams=ParseRouteParams<"/api/user/:userID">;// Params is "userID"typeNoParams=ParseRouteParams<"/api/user">;// NoParams is never: no params!
这已经比我们之前做的好多了。现在,我们想要捕获所有其他可能的参数。为此,我们必须添加另一个条件:
That’s already much better than we did earlier. Now, we want to catch all other possible parameters. For that, we have to add another condition:
typeParseRouteParams<Route>=Routeextends`${string}/:${inferP}/${inferR}`?P|ParseRouteParams<`/${R}`>:Routeextends`${string}/:${inferP}`?P:never;
typeParseRouteParams<Route>=Routeextends`${string}/:${inferP}/${inferR}`?P|ParseRouteParams<`/${R}`>:Routeextends`${string}/:${inferP}`?P:never;
我们的条件类型现在的工作方式如下:
Our conditional type now works as follows:
在第一个条件中,我们检查路由之间是否有路由参数。如果是,我们将提取路由参数及其后的所有内容。我们将新发现的路由参数返回到P联合中,其中我们用其余部分递归调用相同的泛型类型R。例如,如果我们将路由传递"/api/users/:userID/orders/:orderID"给ParseRouteParams,我们会推断"userID"到P和"orders/:orderID"。R我们用调用相同的类型R。
In the first condition, we check if there is a route parameter somewhere in between the route. If so, we extract both the route parameter and everything else that comes after. We return the newly found route parameter P in a union where we call the same generic type recursively with the rest R. For example, if we pass the route "/api/users/:userID/orders/:orderID" to ParseRouteParams, we infer "userID" into P and "orders/:orderID" into R. We call the same type with R.
第二个条件就在这里。在这里我们检查末尾是否有类型。这就是 的情况"orders/:orderID"。我们提取"orderID"并返回这个文字类型。
This is where the second condition comes in. Here we check if there is a type at the end. This is the case for "orders/:orderID". We extract "orderID" and return this literal type.
如果没有剩余的路线参数,我们将返回never:
If there are no more route parameters left, we return never:
// Params is "userID"typeParams=ParseRouteParams<"/api/user/:userID">;// MoreParams is "userID" | "orderID"typeMoreParams=ParseRouteParams<"/api/user/:userID/orders/:orderId">;
// Params is "userID"typeParams=ParseRouteParams<"/api/user/:userID">;// MoreParams is "userID" | "orderID"typeMoreParams=ParseRouteParams<"/api/user/:userID/orders/:orderId">;
让我们应用这种新类型并看看我们的最终用法app.get是什么样的:
Let’s apply this new type and see what our final usage of app.get looks like:
app.get("/api/users/:userID/orders/:orderID",function(req,res){req.params.userID;// Worksreq.params.orderID;// Also available});
app.get("/api/users/:userID/orders/:orderID",function(req,res){req.params.userID;// Worksreq.params.orderID;// Also available});
就是这样!让我们回顾一下。我们刚刚为一个函数创建的类型app.get确保我们排除了大量可能的错误:
And that’s it! Let’s recap. The types we just created for one function app.get make sure that we exclude a ton of possible errors:
我们只能将适当的数字状态代码传递给res.status()。
We can only pass proper numeric status codes to res.status().
req.method是四种可能的字符串之一,当我们使用时app.get,我们知道它只能是"GET"。
req.method is one of four possible strings, and when we use app.get, we know it can only be "GET".
我们可以解析路由参数,并确保回调参数中没有任何拼写错误。
We can parse route params and make sure we don’t have any typos inside our callback parameters.
如果我们查看本节开头的示例,我们会收到以下错误消息:
If we look at the example from the beginning of this recipe, we get the following error messages:
app.get("/api/users/:userID",function(req,res){if(req.method==="POST"){// ^ This condition will always return 'false' since// the types 'Methods' and '"POST"' have no overlap.(2367)res.status(20).send({// ^// Argument of type '20' is not assignable to parameter of// type 'StatusCode'.(2345)message:"Welcome, user "+req.params.userId// ^// Property 'userId' does not exist on type// '{ userID: string; }'. Did you mean 'userID'?});}});
app.get("/api/users/:userID",function(req,res){if(req.method==="POST"){// ^ This condition will always return 'false' since// the types 'Methods' and '"POST"' have no overlap.(2367)res.status(20).send({// ^// Argument of type '20' is not assignable to parameter of// type 'StatusCode'.(2345)message:"Welcome, user "+req.params.userId// ^// Property 'userId' does not exist on type// '{ userID: string; }'. Did you mean 'userID'?});}});
在我们实际运行代码之前,所有这些都已经完成了! Express 样式的服务器是 JavaScript 动态特性的完美示例。 根据您调用的方法和您为第一个参数传递的字符串,回调内部的许多行为都会发生变化。 再举一个例子,您的所有类型看起来都完全不同。
And all that before we actually run our code! Express-style servers are a perfect example of the dynamic nature of JavaScript. Depending on the method you call and the string you pass for the first argument, a lot of behavior changes inside the callback. Take another example and all your types look entirely different.
这种方法的优点在于每一步都增加了更多的类型安全性:
The great thing about this approach is that every step added more type safety:
您可以轻松地停止使用基本类型,并从中获得比 根本没有类型的更多的东西。
You can easily stop at basic types and get more out of it than having no types at all.
通过减少有效值的数量,子集可以帮助您消除拼写错误。
Subsetting helps you get rid of typos by reducing the number of valid values.
泛型可以帮助您根据用例定制行为。
Generics help you tailor behavior to use case.
字符串模板文字类型等高级类型使您的应用在字符串类型的世界中更有意义。
Advanced types like string template literal types give your app more meaning in a stringly-typed world.
锁定泛型允许您使用 JavaScript 中的文字并将其视为类型。
Locking in generics allows you to work with literals in JavaScript and treat them as types.
条件类型使您的类型与 JavaScript 代码一样灵活。
Conditional types make your types as flexible as your JavaScript code.
最好的事情是什么?一旦你添加了类型,人们只需编写纯 JavaScript 即可获得所有类型信息。这对每个人来说都是双赢。
The best thing? Once you added your types, people will just write plain JavaScript and still get all the type information. That’s a win for everybody.
使用satisfies运算符进行类似注释的类型检查,同时保留文字类型。
Use the satisfies operator to do annotation-like type-checking while retaining the literal types.
映射类型很棒,因为它们允许 JavaScript 以灵活性而闻名于对象结构。但它们对类型系统有一些至关重要的影响。以通用消息传递库中的以下示例为例,它采用“通道定义”,其中可以定义多个通道令牌:
Mapped types are great, as they allow for the flexibility in object structures JavaScript is known for. But they have some crucial implications for the type system. Take this example from a generic messaging library, which takes a “channel definition” where multiple channel tokens can be defined:
typeMessages=|"CHANNEL_OPEN"|"CHANNEL_CLOSE"|"CHANNEL_FAIL"|"MESSAGE_CHANNEL_OPEN"|"MESSAGE_CHANNEL_CLOSE"|"MESSAGE_CHANNEL_FAIL";typeChannelDefinition={[key:string]:{open:Messages;close:Messages;fail:Messages;};};
typeMessages=|"CHANNEL_OPEN"|"CHANNEL_CLOSE"|"CHANNEL_FAIL"|"MESSAGE_CHANNEL_OPEN"|"MESSAGE_CHANNEL_CLOSE"|"MESSAGE_CHANNEL_FAIL";typeChannelDefinition={[key:string]:{open:Messages;close:Messages;fail:Messages;};};
此通道定义对象的键是用户想要的。因此,这是一个有效的通道定义:
The keys from this channel definition object are what the user wants them to be. So this is a valid channel definition:
constimpl:ChannelDefinition={test:{open:'CHANNEL_OPEN',close:'CHANNEL_CLOSE',fail:'CHANNEL_FAIL'},message:{open:'MESSAGE_CHANNEL_OPEN',close:'MESSAGE_CHANNEL_CLOSE',fail:'MESSAGE_CHANNEL_FAIL'}}
constimpl:ChannelDefinition={test:{open:'CHANNEL_OPEN',close:'CHANNEL_CLOSE',fail:'CHANNEL_FAIL'},message:{open:'MESSAGE_CHANNEL_OPEN',close:'MESSAGE_CHANNEL_CLOSE',fail:'MESSAGE_CHANNEL_FAIL'}}
然而,我们有一个问题:当我们想要如此灵活地访问我们定义的键时。假设我们有一个打开通道的函数。我们传递整个通道定义对象,以及我们想要打开的通道:
We have a problem, however: when we want to access the keys we defined so flexibly. Let’s say we have a function that opens a channel. We pass the whole channel definition object, as well as the channel we want to open:
functionopenChannel(def:ChannelDefinition,channel:keyofChannelDefinition){// to be implemented}
functionopenChannel(def:ChannelDefinition,channel:keyofChannelDefinition){// to be implemented}
那么 的键是什么ChannelDefinition?嗯,每个键都是:[key: string]。因此,当我们分配特定类型时,TypeScript 会将其视为impl该特定类型,而忽略实际实现。合同已履行。继续。这允许传递错误的键:
So what are the keys of ChannelDefinition? Well, it’s every key: [key: string]. So the moment we assign a specific type, TypeScript treats impl as this specific type, ignoring the actual implementation. The contract is fulfilled. Moving on. This allows for wrong keys to be passed:
// Passes, even though "massage" is not part of implopenChannel(impl,"massage");
// Passes, even though "massage" is not part of implopenChannel(impl,"massage");
因此,我们更感兴趣的是实际的实现,而不是我们分配给常量的类型。这意味着我们必须摆脱类型ChannelDefinition,并确保我们关心对象的实际类型。
So we are more interested in the actual implementation, not the type we assign to our constant. This means we have to get rid of the ChannelDefinition type and make sure we care about the actual type of the object.
首先,该openChannel函数应该接受任何属于子类型的对象ChannelDefinition,但要与具体子类型一起工作:
First, the openChannel function should take any object that is a subtype of ChannelDefinition but work with the concrete subtype:
functionopenChannel<TextendsChannelDefinition>(def:T,channel:keyofT){// to be implemented}
functionopenChannel<TextendsChannelDefinition>(def:T,channel:keyofT){// to be implemented}
TypeScript 现在可以在两个层面上工作:
TypeScript now works on two levels:
它检查是否T真的扩展了ChannelDefinition。如果是,我们就使用类型T。
It checks if T actually extends ChannelDefinition. If so, we work with type T.
我们所有的函数参数都是用泛型类型化的T。这也意味着我们可以通过获得真正的键。Tkeyof T
All our function parameters are typed with the generic T. This also means we get the real keys of T through keyof T.
为了从中受益,我们必须摆脱 的类型定义impl。显式类型定义会覆盖所有实际类型。从我们明确指定类型的那一刻起,TypeScript 就将其视为ChannelDefinition,而不是实际的底层子类型。我们还必须设置const context,这样我们就可以将所有字符串转换为其单位类型(从而符合Messages):
To benefit from that, we have to get rid of the type definition for impl. The explicit type definition overrides all actual types. From the moment we explicitly specify the type, TypeScript treats it as ChannelDefinition, not the actual underlying subtype. We also have to set const context, so we can convert all strings to their unit type (and thus be compliant with Messages):
constimpl={test:{open:"CHANNEL_OPEN",close:"CHANNEL_CLOSE",fail:"CHANNEL_FAIL",},message:{open:"MESSAGE_CHANNEL_OPEN",close:"MESSAGE_CHANNEL_CLOSE",fail:"MESSAGE_CHANNEL_FAIL",},}asconst;
constimpl={test:{open:"CHANNEL_OPEN",close:"CHANNEL_CLOSE",fail:"CHANNEL_FAIL",},message:{open:"MESSAGE_CHANNEL_OPEN",close:"MESSAGE_CHANNEL_CLOSE",fail:"MESSAGE_CHANNEL_FAIL",},}asconst;
如果没有const context,则推断的类型impl为:
Without const context, the inferred type of impl is:
/// typeof impl{test:{open:string;close:string;fail:string;};message:{open:string;close:string;fail:string;};}
/// typeof impl{test:{open:string;close:string;fail:string;};message:{open:string;close:string;fail:string;};}
使用const context,的实际类型impl现在是:
With const context, the actual type of impl is now:
/// typeof impl{test:{readonlyopen:"CHANNEL_OPEN";readonlyclose:"CHANNEL_CLOSE";readonlyfail:"CHANNEL_FAIL";};message:{readonlyopen:"MESSAGE_CHANNEL_OPEN";readonlyclose:"MESSAGE_CHANNEL_CLOSE";readonlyfail:"MESSAGE_CHANNEL_FAIL";};}
/// typeof impl{test:{readonlyopen:"CHANNEL_OPEN";readonlyclose:"CHANNEL_CLOSE";readonlyfail:"CHANNEL_FAIL";};message:{readonlyopen:"MESSAGE_CHANNEL_OPEN";readonlyclose:"MESSAGE_CHANNEL_CLOSE";readonlyfail:"MESSAGE_CHANNEL_FAIL";};}
Const context允许我们满足 制定的契约ChannelDefinition。现在openChannel可以正常工作:
Const context allows us to satisfy the contract made by ChannelDefinition. Now openChannel works correctly:
openChannel(impl,"message");// satisfies contractopenChannel(impl,"massage");// ^// Argument of type '"massage"' is not assignable to parameter// of type '"test" | "message"'.(2345)
openChannel(impl,"message");// satisfies contractopenChannel(impl,"massage");// ^// Argument of type '"massage"' is not assignable to parameter// of type '"test" | "message"'.(2345)
这很有效,但有一个警告。我们唯一可以检查是否impl是有效的子类型的地方ChannelDefinition就是我们在使用它的时候。有时我们想尽早注释以找出合同中可能出现的中断。我们想看看这个特定的实现是否满足合同。
This works but comes with a caveat. The only point where we can check if impl is actually a valid subtype of ChannelDefinition is when we are using it. Sometimes we want to annotate early to figure out potential breaks in our contract. We want to see if this specific implementation satisfies a contract.
幸运的是,有一个关键字可以解决这个问题。我们可以定义对象并进行类型检查,以查看此实现是否满足类型,但 TypeScript 会将其视为文字类型:
Thankfully, there is a keyword for that. We can define objects and do a type-check to see if this implementation satisfies a type, but TypeScript will treat it as a literal type:
constimpl={test:{open:"CHANNEL_OPEN",close:"CHANNEL_CLOSE",fail:"CHANNEL_FAIL",},message:{open:"MESSAGE_CHANNEL_OPEN",close:"MESSAGE_CHANNEL_CLOSE",fail:"MESSAGE_CHANNEL_FAIL",},}satisfiesChannelDefinition;functionopenChannel<TextendsChannelDefinition>(def:T,channel:keyofT){// to be implemented}
constimpl={test:{open:"CHANNEL_OPEN",close:"CHANNEL_CLOSE",fail:"CHANNEL_FAIL",},message:{open:"MESSAGE_CHANNEL_OPEN",close:"MESSAGE_CHANNEL_CLOSE",fail:"MESSAGE_CHANNEL_FAIL",},}satisfiesChannelDefinition;functionopenChannel<TextendsChannelDefinition>(def:T,channel:keyofT){// to be implemented}
这样,我们可以确保履行合同,但具有与const context相同的好处。唯一的区别是字段未设置为readonly,但由于 TypeScript 采用所有内容的文字类型,因此在满足类型检查后无法将字段设置为其他任何内容:
With that, we can make sure that we fulfill contracts but have the same benefits as with const context. The only difference is that the fields are not set to readonly, but since TypeScript takes the literal type of everything, there is no way to set fields to anything else after a satisfaction type-check:
impl.test.close="CHANEL_CLOSE_MASSAGE";// ^// Type '"CHANEL_CLOSE_MASSAGE"' is not assignable// to type '"CHANNEL_CLOSE"'.(2322)
impl.test.close="CHANEL_CLOSE_MASSAGE";// ^// Type '"CHANEL_CLOSE_MASSAGE"' is not assignable// to type '"CHANNEL_CLOSE"'.(2322)
这样,我们就可以两全其美:注释时进行适当的类型检查,以及针对特定情况的缩小类型的功能。
With that, we get the best of both worlds: proper type-checks at annotation time as well as the power of narrowed types for specific situations.
一些常见的辅助类型就像一个测试框架。测试你的类型!
Some commonly known helper types work like a test framework. Test your types!
在动态类型编程语言中,人们总是围绕着这样一个问题展开讨论:如果可以拥有合适的测试套件,是否需要类型。至少一方是这样认为的;另一方则认为,既然可以拥有类型,为什么还要进行那么多测试?答案可能介于两者之间。
In dynamically typed programming languages people always circle around the discussion of if you need types when you can have a proper test suite. This is at least what one camp says; the other thinks, why should we test so much when we can have types? The answer is probably somewhere in the middle.
类型确实可以解决很多测试用例。结果是数字吗?结果是具有某些类型的某些属性的对象吗?我们可以通过类型轻松检查这一点。我的函数是否产生了正确的结果?这些值是我期望的吗?这属于测试。
It is true that types can solve a lot of test cases. Is the result a number? Is the result an object with certain properties of certain types? This is something we can easily check via types. Does my function produce correct results? Are the values what I expect them to be? This belongs to tests.
通过本书,我们学习了很多关于非常复杂的类型的知识。通过条件类型,我们开启了 TypeScript 的元编程功能,我们可以根据以前类型的某些特性来创建新类型。功能强大、图灵完备且非常先进。这引出了一个问题:我们如何确保这些复杂类型确实能做它们应该做的事情?也许我们应该测试我们的类型?
Throughout this book, we learned a lot about very complex types. With conditional types, we opened up the metaprogramming capabilities of TypeScript, where we could craft new types based on certain features of previous types. Powerful, Turing complete, and very advanced. This leads to the question: how do we ensure that those complex types actually do what they should do? Maybe we should test our types?
我们确实可以。社区中已知有几种辅助类型可以作为某种测试框架。以下类型来自出色的Type Challenges 存储库,它允许您最大限度地测试您的 TypeScript 类型系统技能。它们包括非常具有挑战性的任务:一些与实际用例相关,而另一些只是为了好玩。
We actually can. There are a few helper types known within the community that can serve as some sort of testing framework. The following types come from the excellent Type Challenges repository, which allows you to test your TypeScript type system skills to an extreme. They include very challenging tasks: some that have relevance to real-world use cases and others that are just for fun.
他们的测试库以几种需要真值或假值的类型开始。它们非常简单。通过使用泛型和文字类型,我们可以检查这个布尔值是真还是假:
Their testing library starts with a few types that expect a truthy or a falsy value. They are pretty straightforward. By using generics and literal types, we can check if this one Boolean is true or false:
exporttypeExpect<Textendstrue>=T;exporttypeExpectTrue<Textendstrue>=T;exporttypeExpectFalse<Textendsfalse>=T;exporttypeIsTrue<Textendstrue>=T;exporttypeIsFalse<Textendsfalse>=T;
exporttypeExpect<Textendstrue>=T;exporttypeExpectTrue<Textendstrue>=T;exporttypeExpectFalse<Textendsfalse>=T;exporttypeIsTrue<Textendstrue>=T;exporttypeIsFalse<Textendsfalse>=T;
它们本身并不能做很多事情,但与Equal<X, Y>和 一起使用时效果非常好NotEqual<X, Y>,它们会返回true或false:
They don’t do much on their own but are fantastic when being used with Equal<X, Y> and NotEqual<X, Y>, which return either true or false:
exporttypeEqual<X,Y>=(<T>()=>TextendsX?1:2)extends(<T>()=>TextendsY?1:2)?true:false;exporttypeNotEqual<X,Y>=trueextendsEqual<X,Y>?false:true;
exporttypeEqual<X,Y>=(<T>()=>TextendsX?1:2)extends(<T>()=>TextendsY?1:2)?true:false;exporttypeNotEqual<X,Y>=trueextendsEqual<X,Y>?false:true;
Equal<X, Y>很有趣,因为它创建了通用函数并根据应该相互比较的两种类型检查它们。由于每种条件类型都没有解决方案,因此 TypeScript 会比较两种条件类型并查看是否存在兼容性。这是 TypeScript 条件类型逻辑中的一个步骤,Alex Chashin 在 Stack Overflow 上对此进行了精彩的解释。
Equal<X, Y> is interesting as it creates generic functions and checks them against both types that should be compared with each other. Since there is no resolution on each conditional type, TypeScript compares both conditional types and can see if there is compatibility. It’s a step within TypeScript’s conditional type logic that is masterfully explained by Alex Chashin on Stack Overflow.
The next batch allows us to check if a type is any:
exporttypeIsAny<T>=0extends1&T?true:false;exporttypeNotAny<T>=trueextendsIsAny<T>?false:true;
exporttypeIsAny<T>=0extends1&T?true:false;exporttypeNotAny<T>=trueextendsIsAny<T>?false:true;
它是一种简单的条件类型,0用于检查1 & T,它应该始终缩小到1或never,这始终会产生false条件类型的分支。除非我们与 相交any。与 的交集any始终是any,并且0是 的子集any。
It’s a simple conditional type that checks 0 against 1 & T, which should always narrow down to 1 or never, which always yields the false branch of the conditional type. Except when we intersect with any. An intersection with any is always any, and 0 is a subset of any.
下一批是对我们8.3 节Remap中看到的和的重新解释,以及用于比较结构相同但构造不同的类型的方法
:DeepRemapAlike
The next batch is reinterpretations of Remap and DeepRemap we saw in Recipe 8.3, along with Alike as a way to compare types that are equal in structure but not
construction:
exporttypeDebug<T>={[KinkeyofT]:T[K]};exporttypeMergeInsertions<T>=Textendsobject?{[KinkeyofT]:MergeInsertions<T[K]>}:T;exporttypeAlike<X,Y>=Equal<MergeInsertions<X>,MergeInsertions<Y>>;
exporttypeDebug<T>={[KinkeyofT]:T[K]};exporttypeMergeInsertions<T>=Textendsobject?{[KinkeyofT]:MergeInsertions<T[K]>}:T;exporttypeAlike<X,Y>=Equal<MergeInsertions<X>,MergeInsertions<Y>>;
理论上,之前的检查Equal应该能够理解{ x : number, y: string }等于{ x: number } & { y: string },但 TypeScript 类型检查器的实现细节并不认为它们相等。这就是Alike发挥作用的地方。
The Equal check before should theoretically be able to understand that { x : number, y: string } is equal to { x: number } & { y: string }, but implementation details of the TypeScript type-checker don’t see them as equal. That’s where Alike comes into play.
类型挑战测试文件的最后一批做了两件事:
The last batch of the type challenges testing file does two things:
它使用简单的条件类型进行子集检查。
It does subset checks with a simple conditional type.
它检查你构造的元组是否可以看作是函数的有效参数:
It checks if a tuple you have constructed can be seen as a valid argument for a function:
exporttypeExpectExtends<VALUE,EXPECTED>=EXPECTEDextendsVALUE?true:false;exporttypeExpectValidArgs<FUNCextends(...args:any[])=>any,ARGSextendsany[]>=ARGSextendsParameters<FUNC>?true:false;
exporttypeExpectExtends<VALUE,EXPECTED>=EXPECTEDextendsVALUE?true:false;exporttypeExpectValidArgs<FUNCextends(...args:any[])=>any,ARGSextendsany[]>=ARGSextendsParameters<FUNC>?true:false;
当你的类型越来越复杂时,拥有一个像这样的小型辅助类型库来进行类型测试和调试确实很有帮助。将它们添加到你的全局类型定义文件中(参见范例 9.7)并使用它们。
Having a small helper type library like this for type testing and debugging is really helpful when your types get more complex. Add them to your global type definition files (see Recipe 9.7) and use them.
使用名为Zod 的库定义模式并使用它来验证来自外部源的数据。
Define schemas using a library called Zod and use it to validate data from external sources.
恭喜!我们快结束了。如果你从头到尾都跟着我,你就会不断被提醒 TypeScript 的类型系统遵循几个目标。首先,它希望为您提供出色的工具,以便您在开发应用程序时能够 高效工作。它还希望迎合所有 JavaScript 框架,并确保它们有趣且易于使用。它将自己视为 JavaScript 的附加组件,作为静态类型的语法。也有一些非目标或权衡。它更喜欢生产力而不是正确性,它允许开发人员根据自己的需要改变规则,并且它不声称自己是可证明的健全的。
Congratulations! We’re almost at the end. If you have followed along from start to finish, you have been constantly reminded that TypeScript’s type system follows a couple of goals. First and foremost, it wants to give you excellent tooling so you can be productive when developing applications. It also wants to cater to all JavaScript frameworks and make sure they are fun and easy to use. It sees itself as an add-on to JavaScript, as a syntax for static types. There are also some non-goals or trade-offs. It prefers productivity over correctness, it allows developers to bend the rules to their needs, and it has no claim of being provably sound.
在范例 3.9中,我们了解到,如果我们认为类型应该有所不同,我们可以通过类型断言来影响 TypeScript ;在范例 9.2中,我们了解到如何使不安全的操作更加健壮且更容易发现。由于 TypeScript 的类型系统仅在编译时,因此一旦我们在所选的运行时中运行 JavaScript,所有安全措施都将消失。
In Recipe 3.9 we learned that we can influence TypeScript if we think that types should be something different through type assertions, and in Recipe 9.2 we learned how we can make unsafe operations more robust and easier to spot. Since TypeScript’s type system is compile-time only, all our safeguards evaporate once we run JavaScript in our selected runtime.
通常,编译时类型检查就足够了。只要我们在编写自己的类型的内部世界中,让 TypeScript 检查一切是否正常,我们的代码是否可以使用。然而,在 JavaScript 应用程序中,我们还要处理很多我们无法控制的事情:例如用户输入。我们需要访问和处理的第三方 API。不可避免地,我们在开发过程中会到达一个点,我们需要离开我们类型良好的应用程序的界限,处理我们不能信任的数据。
Usually, compile-time type-checks are good enough. As long as we are within the inner world where we write our own types, let TypeScript check that everything is OK, and our code is good to go. In JavaScript applications, however, we also deal with a lot of things beyond our control: user input, for example. APIs from third parties that we need to access and process. Inevitably, we reach a point in our development process where we need to leave the boundaries of our well-typed application and deal with data that we can’t trust.
在开发过程中,使用外部来源或用户输入可能效果很好,但要确保我们使用的数据在生产中保持不变,则需要付出额外的努力。您可能需要验证您的数据是否符合某种方案。
While developing, working with external sources or user input might work well enough, but to make sure that the data we use stays the same when running in production requires extra effort. You may want to validate that your data adheres to a certain scheme.
值得庆幸的是,有一些库可以处理这类任务。近年来,Zod库越来越受欢迎。Zod 是 TypeScript 优先的,这意味着它不仅确保您使用的数据有效且符合您的预期,而且还确保您获得可在整个程序中使用的 TypeScript 类型。Zod 将自己视为您无法控制的外部世界与内部世界之间的守卫,在内部世界中,一切都经过了类型良好和类型检查。
Thankfully, there are libraries that deal with that kind of task. One library that has gained popularity in recent years is Zod. Zod is TypeScript-first, which means it makes sure not only that the data you consume is valid and what you expect but also that you get TypeScript types you can use throughout your program. Zod sees itself as the guard between the outer world outside of your control and the inner world where everything is well-typed and also type-checked.
想象一下,一个 API 为您提供Person我们在书中看到的类型的数据。APerson有姓名和年龄、可选的职业以及状态:在我们的系统中,它们可以是活跃的、不活跃的,也可以是仅注册的,等待确认。
Think of an API that gives you data for the Person type we’ve seen throughout the book. A Person has a name and age, a profession that is optional, and also a status: in our system, they can be either active, inactive, or only registered, waiting for confirmation.
该 API 还将几个Person对象打包在类型内的数组中Result。简而言之,这是 HTTP 调用的经典响应类型的示例:
The API also packs a couple of Person objects in an array contained within a Result type. In short, it’s an example for a classic response type for HTTP calls:
typePerson={name:string;age:number;profession?:string|undefined;status:"active"|"inactive"|"registered";};typeResults={entries:Person[]};
typePerson={name:string;age:number;profession?:string|undefined;status:"active"|"inactive"|"registered";};typeResults={entries:Person[]};
您知道如何像这样对模型进行类型化。到目前为止,您已经能够熟练地识别和应用语法和模式。我们希望拥有相同的类型,但在运行时,对于我们无法控制的数据,我们使用 Zod。在 JavaScript 中编写相同的类型(值命名空间)看起来非常熟悉:
You know how to type models like this. By now, you are fluent in recognizing and applying both syntax and patterns. We want to have the same type, but at runtime for data outside our control, we use Zod. And writing the same type in JavaScript (the value namespace) looks very familiar:
import{z}from"zod";constPerson=z.object({name:z.string(),age:z.number().min(0).max(150),profession:z.string().optional(),status:z.union([z.literal("active"),z.literal("inactive"),z.literal("registered"),]),});constResults=z.object({entries:z.array(Person),});
import{z}from"zod";constPerson=z.object({name:z.string(),age:z.number().min(0).max(150),profession:z.string().optional(),status:z.union([z.literal("active"),z.literal("inactive"),z.literal("registered"),]),});constResults=z.object({entries:z.array(Person),});
如您所见,我们在 JavaScript 中,我们将名称添加到值命名空间,而不是类型命名空间(参见方案 2.9),但我们从 Zod 的流畅界面中获得的工具对于我们 TypeScript 开发人员来说非常熟悉。我们定义对象、字符串、数字和数组。我们还可以定义联合类型和文字。定义模型的所有构建块都在这里,我们还可以嵌套类型,正如我们所看到的,通过Person先定义并在中重用它Results。
As you see, we are in JavaScript, and we add names to the value namespace, not the type namespace (see Recipe 2.9), but the tools we get from Zod’s fluent interface are very familiar to us TypeScript developers. We define objects, strings, numbers, and arrays. We can also define union types and literals. All the building blocks for defining models are here, and we can also nest types, as we see by defining Person first and reusing it in Results.
流畅接口还允许我们将某些属性设为可选。所有这些都是我们从 TypeScript 中了解到的。此外,我们可以设置验证规则。我们可以说年龄应该大于或等于 0 且小于 100。这些是我们无法在类型系统中合理做到的事情。
The fluent interface also allows us to make certain properties optional. All things that we know from TypeScript. Furthermore, we can set validation rules. We can say that age should be above or equal to 0 and below 100. Things that we can’t do reasonably within the type system.
这些对象不是我们可以像使用 TypeScript 类型那样使用的类型。它们是模式,等待它们可以解析和验证的数据。由于 Zod 是 TypeScript-first,我们有辅助类型,允许我们从值空间跨越到类型空间。使用z.infer(类型,而不是函数),我们可以通过 Zod 的模式函数提取我们定义的类型:
Those objects are not types that we can use like we would use TypeScript types. They are schemas, waiting for data they can parse and validate. Since Zod is TypeScript-first, we have helper types that allow us to cross the bridge from the value space to the type space. With z.infer (a type, not a function), we can extract the type we defined through Zod’s schema functions:
typePersonType=z.infer<typeofPerson>;typeResultType=z.infer<typeofResults>;
typePersonType=z.infer<typeofPerson>;typeResultType=z.infer<typeofResults>;
那么,我们如何应用 Zod 的验证技术?让我们讨论一个名为 的函数fetchData,它调用一个获取 类型条目的 API ResultType。我们只是不知道我们收到的值是否真正符合我们定义的类型。因此,在将数据作为 提取后json,我们使用Results模式来解析我们收到的数据。如果此过程成功,我们将获得 类型的数据ResultType:
So, how do we apply Zod’s validation techniques? Let’s talk about a function called fetchData, which calls an API that gets entries of type ResultType. We just don’t know if the values we receive actually adhere to the types we’ve defined. So, after fetching data as json, we use the Results schema to parse the data we’ve received. If this process is successful, we get data that is of type ResultType:
typeResultType=z.infer<typeofResults>;asyncfunctionfetchData():Promise<ResultType>{constdata=awaitfetch("/api/persons").then((res)=>res.json());returnResults.parse(data);}
typeResultType=z.infer<typeofResults>;asyncfunctionfetchData():Promise<ResultType>{constdata=awaitfetch("/api/persons").then((res)=>res.json());returnResults.parse(data);}
请注意,我们在定义函数接口时已经有了第一个保障。Promise<ResultType>是基于我们从中获得的内容z.infer。
Note that we already had our first safeguard in how we defined the function interface. Promise<ResultType> is based on what we get from z.infer.
Results.parse(data)是推断类型,但没有名称。结构类型系统确保我们返回正确的内容。可能会有错误,我们可以使用相应的方法或块catch来纠正它们。Promise.catchtrycatch
Results.parse(data) is of the inferred type but without a name. The structural type system makes sure that we return the right thing. There might be errors, and we can catch them using the respective Promise.catch methods or try-catch blocks.
try与-一起使用catch:
Usage with try-catch:
fetchData().then((res)=>{// do something with results}).catch((e)=>{// a potential zod error!});// ortry{constres=awaitfetchData();// do something with results}catch(e){// a potential zod error!}
fetchData().then((res)=>{// do something with results}).catch((e)=>{// a potential zod error!});// ortry{constres=awaitfetchData();// do something with results}catch(e){// a potential zod error!}
虽然我们可以确保只有在数据正确的情况下才能继续,但我们不必进行错误检查。如果我们想确保在继续执行程序之前先查看解析结果,safeParse那么可以这样做:
While we can ensure that we continue only if we have correct data, we are not forced to do error checking. If we want to make sure that we look at the parsing result first before we continue with our program, safeParse is the way to go:
asyncfunctionfetchData():Promise<ResultType>{constdata=awaitfetch("/api/persons").then((res)=>res.json());constresults=Results.safeParse(data);if(results.success){returnresults.data;}else{// Depending on your application, you might want to have a// more sophisticated way of error handling than returning// an empty result.return{entries:[]};}}
asyncfunctionfetchData():Promise<ResultType>{constdata=awaitfetch("/api/persons").then((res)=>res.json());constresults=Results.safeParse(data);if(results.success){returnresults.data;}else{// Depending on your application, you might want to have a// more sophisticated way of error handling than returning// an empty result.return{entries:[]};}}
如果您需要依赖外部数据,这已经使 Zod 成为一项宝贵资产。此外,它还允许您适应 API 更改。假设您的程序只能在 的活动和非活动状态下工作Person;它不知道如何处理
registered。应用转换很容易,根据您获得的数据,您可以将状态修改"registered"为"active":
This already makes Zod a valuable asset if you need to rely on external data. Furthermore, it allows you to adapt to API changes. Let’s say that your program can work only with active and inactive states of Person; it does not know how to handle
registered. It’s easy to apply a transform where, based on the data you get, you modify the "registered" state to be actually "active":
constPerson=z.object({name:z.string(),age:z.number().min(0).max(150),profession:z.string().optional(),status:z.union([z.literal("active"),z.literal("inactive"),z.literal("registered"),]).transform((val)=>{if(val==="registered"){return"active";}returnval;}),});
constPerson=z.object({name:z.string(),age:z.number().min(0).max(150),profession:z.string().optional(),status:z.union([z.literal("active"),z.literal("inactive"),z.literal("registered"),]).transform((val)=>{if(val==="registered"){return"active";}returnval;}),});
然后,您将使用两种不同的类型:输入类型表示 API 提供的内容,输出类型是解析后的数据。幸运的是,我们可以从相应的 Zod 辅助类型z.input和中获取这两种类型z.output:
You then work with two different types: the input type represents what the API is giving you, and the output type is the data you have after parsing. Thankfully, we can get both types from the respective Zod helper types z.input and z.output:
typePersonTypeIn=z.input<typeofPerson>;/*type PersonTypeIn = {name: string;age: number;profession?: string | undefined;status: "active" | "inactive" | "registered";};*/typePersonTypeOut=z.output<typeofPerson>;/*type PersonTypeOut = {name: string;age: number;profession?: string | undefined;status: "active" | "inactive";};*/
typePersonTypeIn=z.input<typeofPerson>;/*type PersonTypeIn = {name: string;age: number;profession?: string | undefined;status: "active" | "inactive" | "registered";};*/typePersonTypeOut=z.output<typeofPerson>;/*type PersonTypeOut = {name: string;age: number;profession?: string | undefined;status: "active" | "inactive";};*/
Zod 的打字非常聪明,能够理解您从中删除了三个文字中的一个status。因此,没有什么意外,您实际上处理的是您一直期待的数据。
Zod’s typings are clever enough to understand that you removed one of the three literals from status. So there are no surprises and you actually deal with the data you’ve been expecting.
Zod 的 API 优雅、易用,与 TypeScript 的功能紧密结合。对于您无法控制的边界数据,您需要依赖第三方提供预期的数据形状,Zod 可以帮您大忙,您无需做太多工作。不过,这需要付出代价:运行时验证需要时间。数据集越大,所需的时间越长。此外,12KB 的数据量也很大。请确保您需要对边界数据进行这种验证。
Zod’s API is elegant, easy to use, and closely aligned with TypeScript’s features. For data at the boundaries that you can’t control, where you need to rely on third parties to provide the expected shape of data, Zod is a lifesaver without you having to do too much work. It comes at a cost, though: runtime validation takes time. The bigger the dataset, the longer it takes. Also, at 12KB it’s big. Be certain that you need this kind of validation for data at your boundaries.
如果您请求的数据来自公司内的其他团队,那么坐在您旁边的人、没有图书馆,甚至 Zod 都比彼此交谈和协作实现相同目标更胜一筹。类型是指导协作的一种方式,而不是摆脱协作的手段。
If the data you request comes from some other team within your company, maybe the person sitting next to you, no library, not even Zod, beats talking with each other and collaborating toward the same goals. Types are a way to guide collaboration, not a means to get rid of it.
TypeScript 寻找可能值的最小公分母。使用泛型类型锁定特定键,这样 TypeScript 就不会假设规则需要适用于所有键。
TypeScript looks for the lowest common denominator of possible values. Use a generic type to lock in specific keys so TypeScript doesn’t assume the rule needs to apply for all.
有时在编写 TypeScript 时,您通常在 JavaScript 中执行的操作会略有不同,并导致一些奇怪且令人费解的情况。有时您只是想通过索引访问为对象属性分配一个值,但却收到错误,例如“类型'string | number'无法分配给类型'never'。类型'string'无法分配给类型'never'。(2322)。”
Sometimes when writing TypeScript, actions you’d usually do in JavaScript work a little differently and cause some weird and puzzling situations. Sometimes you just want to assign a value to an object property via index access and get an error like “Type 'string | number' is not assignable to type 'never'. Type 'string' is not assignable to type 'never'.(2322).”
这没有什么不寻常的;只是“意外的交叉类型”让你对类型系统多一点思考。
This isn’t out of the ordinary; it’s just where “unexpected intersection types” make you think a little bit more about the type system.
让我们看这个例子。我们创建一个函数,让我们通过提供一个键从一个对象更新anotherPerson到另一个对象。和都具有相同的类型,但 TypeScript 会抛出错误:personpersonanotherPersonPerson
Let’s look at this example. We create a function that lets us update from one object anotherPerson to object person via providing a key. Both person and anotherPerson have the same type Person, but TypeScript throws errors:
letperson={name:"Stefan",age:39,};typePerson=typeofperson;letanotherPerson:Person={name:"Not Stefan",age:20,};functionupdate(key:keyofPerson){person[key]=anotherPerson[key];//^ Type 'string | number' is not assignable to type 'never'.// Type 'string' is not assignable to type 'never'.(2322)}update("age");
letperson={name:"Stefan",age:39,};typePerson=typeofperson;letanotherPerson:Person={name:"Not Stefan",age:20,};functionupdate(key:keyofPerson){person[key]=anotherPerson[key];//^ Type 'string | number' is not assignable to type 'never'.// Type 'string' is not assignable to type 'never'.(2322)}update("age");
通过索引访问运算符进行的属性赋值对于 TypeScript 来说很难追踪。即使你通过 缩小了所有可能的访问键的范围keyof Person,可以分配的可能值也是string或number(分别表示姓名和年龄)。虽然如果你在语句的右侧有索引访问(读取),这没问题,但如果你在语句的左侧有索引访问(写入),情况就会变得有点有趣。
Property assignments via the index access operator are hard for TypeScript to track down. Even if you narrow all possible access keys via keyof Person, the possible values that can be assigned are string or number (for name and age, respectively). While this is fine if you have index access on the righthand side of a statement (reading), it gets a little interesting if you have index access on the lefthand side of a statement (writing).
TypeScript 无法保证你传递的值确实正确。看看这个函数签名:
TypeScript can’t guarantee that the value you pass along is actually correct. Look at this function signature:
functionupdateAmbiguous(key:keyofPerson,value:Person[keyofPerson]){//...}updateAmbiguous("age","Stefan");
functionupdateAmbiguous(key:keyofPerson,value:Person[keyofPerson]){//...}updateAmbiguous("age","Stefan");
没有什么可以阻止我为每个键添加错误类型的值。除了 TypeScript,它会抛出一个错误。但是为什么 TypeScript 告诉我们类型是never?
Nothing prevents me from adding a falsely typed value to every key. Except for TypeScript, which throws an error. But why does TypeScript tell us the type is never?
为了允许某些赋值,TypeScript 做出了妥协。TypeScript 不会完全不允许右侧的任何赋值,而是寻找可能值的最小公分母,例如:
To allow for some assignments TypeScript compromises. Instead of not allowing any assignments at all on the righthand side, TypeScript looks for the lowest common denominator of possible values, for example:
typeSwitch={address:number,on:0|1};declareconstswitcher:Switch;declareconstkey:keyofSwitch;
typeSwitch={address:number,on:0|1};declareconstswitcher:Switch;declareconstkey:keyofSwitch;
这里,两个键都是 的子集number。address是整个数字集;on另一边是0或1。 完全可以将0或1设置为两个字段! 这也是使用 TypeScript 所获得的结果:
Here, both keys are subsets of number. address is the entire set of numbers; on on the other side is either 0 or 1. It’s absolutely possible to set 0 or 1 to both fields! And this is what you get with TypeScript as well:
switcher[key]=1;// This worksswitcher[key]=2;// Error// ^ Type '2' is not assignable to type '0 | 1'.(2322)
switcher[key]=1;// This worksswitcher[key]=2;// Error// ^ Type '2' is not assignable to type '0 | 1'.(2322)
TypeScript 通过对所有属性类型进行交集类型计算来获取可能的可赋值。对于Switch,它是number & (0 | 1),归结为0 | 1。对于所有Person属性,它是string & number,没有重叠;因此它是never。哈哈!罪魁祸首就是它!
TypeScript gets to the possible assignable values by doing an intersection type of all property types. In the case of the Switch, it’s number & (0 | 1), which boils down to 0 | 1. In the case of all Person properties, it’s string & number, which has no overlap; therefore it’s never. Hah! There’s the culprit!
绕过这种严格性的一种方法(这对您自己有好处)是使用泛型。keyof Person我们不是允许所有值访问,而是将特定的子集绑定keyof Person到泛型变量:
One way to get around this strictness (which is for your own good) is by using generics. Instead of allowing all keyof Person values to access, we bind a specific subset of keyof Person to a generic variable:
functionupdate<KextendskeyofPerson>(key:K){person[key]=anotherPerson[key];// works}update("age");
functionupdate<KextendskeyofPerson>(key:K){person[key]=anotherPerson[key];// works}update("age");
当 I 时update("age"),K必定是 的字面类型"age"。这里不存在歧义!
When I update("age"), K is bound to the literal type of "age". No ambiguity there!
这存在一个理论上的漏洞,因为我们可以update用更广泛的通用值来实例化:
There is a theoretical loophole since we could instantiate update with a much broader generic value:
update<"age"|"name">("age");
update<"age"|"name">("age");
目前,这是 TypeScript 团队允许的。另请参阅Anders Hejlsberg 的评论。请注意,他要求查看此类场景的用例,该用例完美地详细说明了 TypeScript 团队的工作方式。通过右侧的索引访问进行的原始分配存在很大的错误可能性,因此他们为您提供了足够的保护措施,直到您非常有意识地执行您想要执行的操作。这可以排除整个错误类别而不会造成太多阻碍。
This is something the TypeScript team allows, for now. See also this comment by Anders Hejlsberg. Note that he asks to see use cases for such a scenario, which perfectly details how the TypeScript team works. The original assignment via index access on the righthand side has so much potential for error that they give you enough safeguards until you make it very intentional what you want to do. This is ruling out entire classes of errors without getting too much in the way.
与条件语句相比,函数重载提供了更好的可读性,并且更容易定义类型的期望。在情况需要时使用它们。
Function overloads provide better readability and an easier way to define expectations from your type than conditionals. Use them when the situation requires.
随着条件类型或可变元组类型等类型系统功能的出现,一种描述函数接口的技术逐渐淡出人们的视线:函数 重载。这是有原因的。这两种功能都是为了解决常规函数重载的缺点而实现的。
With type system features like conditional types or variadic tuple types, one technique to describe a function’s interface has faded into the background: function overloads. And for good reason. Both features have been implemented to deal with the shortcomings of regular function overloads.
直接从 TypeScript 4.0 发行说明中查看此连接示例。这是一个数组concat函数:
See this concatenation example directly from the TypeScript 4.0 release notes. This is an array concat function:
functionconcat(arr1,arr2){return[...arr1,...arr2];}
functionconcat(arr1,arr2){return[...arr1,...arr2];}
为了正确地输入这样的函数,以便考虑到所有可能的边缘情况,我们最终会陷入大量的过载:
To correctly type a function like this so it takes all possible edge cases into account, we would end up in a sea of overloads:
// 7 overloads for an empty second arrayfunctionconcat(arr1:[],arr2:[]):[];functionconcat<A>(arr1:[A],arr2:[]):[A];functionconcat<A,B>(arr1:[A,B],arr2:[]):[A,B];functionconcat<A,B,C>(arr1:[A,B,C],arr2:[]):[A,B,C];functionconcat<A,B,C,D>(arr1:[A,B,C,D],arr2:[]):[A,B,C,D];functionconcat<A,B,C,D,E>(arr1:[A,B,C,D,E],arr2:[]):[A,B,C,D,E];functionconcat<A,B,C,D,E,F>(arr1:[A,B,C,D,E,F],arr2:[]):[A,B,C,D,E,F];// 7 more for arr2 having one elementfunctionconcat<A2>(arr1:[],arr2:[A2]):[A2];functionconcat<A1,A2>(arr1:[A1],arr2:[A2]):[A1,A2];functionconcat<A1,B1,A2>(arr1:[A1,B1],arr2:[A2]):[A1,B1,A2];functionconcat<A1,B1,C1,A2>(arr1:[A1,B1,C1],arr2:[A2]):[A1,B1,C1,A2];functionconcat<A1,B1,C1,D1,A2>(arr1:[A1,B1,C1,D1],arr2:[A2]):[A1,B1,C1,D1,A2];functionconcat<A1,B1,C1,D1,E1,A2>(arr1:[A1,B1,C1,D1,E1],arr2:[A2]):[A1,B1,C1,D1,E1,A2];functionconcat<A1,B1,C1,D1,E1,F1,A2>(arr1:[A1,B1,C1,D1,E1,F1],arr2:[A2]):[A1,B1,C1,D1,E1,F1,A2];// and so on, and so forth
// 7 overloads for an empty second arrayfunctionconcat(arr1:[],arr2:[]):[];functionconcat<A>(arr1:[A],arr2:[]):[A];functionconcat<A,B>(arr1:[A,B],arr2:[]):[A,B];functionconcat<A,B,C>(arr1:[A,B,C],arr2:[]):[A,B,C];functionconcat<A,B,C,D>(arr1:[A,B,C,D],arr2:[]):[A,B,C,D];functionconcat<A,B,C,D,E>(arr1:[A,B,C,D,E],arr2:[]):[A,B,C,D,E];functionconcat<A,B,C,D,E,F>(arr1:[A,B,C,D,E,F],arr2:[]):[A,B,C,D,E,F];// 7 more for arr2 having one elementfunctionconcat<A2>(arr1:[],arr2:[A2]):[A2];functionconcat<A1,A2>(arr1:[A1],arr2:[A2]):[A1,A2];functionconcat<A1,B1,A2>(arr1:[A1,B1],arr2:[A2]):[A1,B1,A2];functionconcat<A1,B1,C1,A2>(arr1:[A1,B1,C1],arr2:[A2]):[A1,B1,C1,A2];functionconcat<A1,B1,C1,D1,A2>(arr1:[A1,B1,C1,D1],arr2:[A2]):[A1,B1,C1,D1,A2];functionconcat<A1,B1,C1,D1,E1,A2>(arr1:[A1,B1,C1,D1,E1],arr2:[A2]):[A1,B1,C1,D1,E1,A2];functionconcat<A1,B1,C1,D1,E1,F1,A2>(arr1:[A1,B1,C1,D1,E1,F1],arr2:[A2]):[A1,B1,C1,D1,E1,F1,A2];// and so on, and so forth
而且这只考虑了最多有六个元素的数组。可变元组类型在以下情况下非常有用:
And this only takes into account arrays that have up to six elements. Variadic tuple types help greatly with these situations:
typeArr=readonlyany[];functionconcat<TextendsArr,UextendsArr>(arr1:T,arr2:U):[...T,...U]{return[...arr1,...arr2];}
typeArr=readonlyany[];functionconcat<TextendsArr,UextendsArr>(arr1:T,arr2:U):[...T,...U]{return[...arr1,...arr2];}
新的函数签名需要的解析工作量少了很多,并且非常清楚它期望获取哪些类型作为参数以及返回什么。返回值也映射到返回类型。没有额外的断言:TypeScript 可以确保您返回正确的值。
The new function signature requires a lot less effort to parse and is very clear on what types it expects to get as arguments and what it returns. The return value also maps to the return type. No extra assertions: TypeScript can make sure that you are returning the correct value.
条件类型的情况也类似。此示例与方案 5.1非常相似。想象一下根据客户、文章或订单 ID 检索订单的软件。您可能想要创建类似以下内容的内容:
It’s a similar situation with conditional types. This example is very similar to Recipe 5.1. Think of software that retrieves orders based on customer, article, or order ID. You might want to create something like this:
functionfetchOrder(customer:Customer):Order[]functionfetchOrder(product:Product):Order[]functionfetchOrder(orderId:number):Order// the implementationfunctionfetchOrder(param:any):Order|Order[]{//...}
functionfetchOrder(customer:Customer):Order[]functionfetchOrder(product:Product):Order[]functionfetchOrder(orderId:number):Order// the implementationfunctionfetchOrder(param:any):Order|Order[]{//...}
但这只是事实的一半。如果你最终得到模棱两可的类型,不知道你得到的是只有
aCustomer还是只有 a ,该怎么办Product?你需要注意所有可能的组合:
But this is just half the truth. What if you end up with ambiguous types where you don’t know exactly if you get only
a Customer or only a Product? You need to take care of all possible combinations:
functionfetchOrder(customer:Customer):Order[]functionfetchOrder(product:Product):Order[]functionfetchOrder(orderId:number):OrderfunctionfetchOrder(param:Customer|Product):Order[]functionfetchOrder(param:Customer|number):Order|Order[]functionfetchOrder(param:number|Product):Order|Order[]// the implementationfunctionfetchOrder(param:any):Order|Order[]{//...}
functionfetchOrder(customer:Customer):Order[]functionfetchOrder(product:Product):Order[]functionfetchOrder(orderId:number):OrderfunctionfetchOrder(param:Customer|Product):Order[]functionfetchOrder(param:Customer|number):Order|Order[]functionfetchOrder(param:number|Product):Order|Order[]// the implementationfunctionfetchOrder(param:any):Order|Order[]{//...}
添加更多可能性,最终会得到更多组合。在这里,条件类型可以极大地减少你的函数签名:
Add more possibilities, and you end up with more combinations. Here, conditional types can reduce your function signature tremendously:
typeFetchParams=number|Customer|Product;typeFetchReturn<T>=TextendsCustomer?Order[]:TextendsProduct?Order[]:Textendsnumber?Order:never;functionfetchOrder<TextendsFetchParams>(params:T):FetchReturn<T>{//...}
typeFetchParams=number|Customer|Product;typeFetchReturn<T>=TextendsCustomer?Order[]:TextendsProduct?Order[]:Textendsnumber?Order:never;functionfetchOrder<TextendsFetchParams>(params:T):FetchReturn<T>{//...}
由于条件类型分发联合,因此FetchReturn返回返回类型的联合。
Since conditional types distribute a union, FetchReturn returns a union of return types.
因此,我们有充分的理由使用这些技术,而不是淹没在过多的函数重载中。那么,回到问题:我们还需要函数重载吗?
So there is good reason to use those techniques instead of drowning in too many function overloads. So, to return to the question: do we still need function overloads?
是的,我们知道。
Yes, we do.
函数重载仍然很方便的一种情况是,如果你的函数变体有不同的参数列表。这意味着不仅参数(参数)本身可以有一些变化(这是条件和可变元组的妙处),而且参数的数量和位置也可以有变化。
One scenario where function overloads remain handy is if you have different argument lists for your function variants. This means not only the arguments (parameters) themselves can have some variety (this is where conditionals and variadic tuples are fantastic) but also the number and position of arguments.
想象一下,一个搜索函数有两种不同的调用方式:
Imagine a search function that has two different ways of being called:
使用搜索查询来调用它。它会返回一个Promise您可以等待的内容。
Call it with the search query. It returns a Promise you can await.
使用搜索查询和回调来调用它。在这种情况下,该函数不返回任何内容。
Call it with the search query and a callback. In this scenario, the function does not return anything.
这可以通过条件类型来完成,但非常笨重:
This can be done with conditional types but is very unwieldy:
// => (1)typeSearchArguments=// Argument list one: a query and a callback|[query:string,callback:(results:unknown[])=>void]// Argument list two:: just a query|[query:string];// A conditional type picking either void or a Promise depending// on the input => (2)typeReturnSearch<T>=Textends[query:string]?Promise<Array<unknown>>:void;// the actual function => (3)declarefunctionsearch<TextendsSearchArguments>(...args:T):ReturnSearch<T>;// z is voidconstz=search("omikron",(res)=>{});// y is Promise<unknown>consty=search("omikron");
// => (1)typeSearchArguments=// Argument list one: a query and a callback|[query:string,callback:(results:unknown[])=>void]// Argument list two:: just a query|[query:string];// A conditional type picking either void or a Promise depending// on the input => (2)typeReturnSearch<T>=Textends[query:string]?Promise<Array<unknown>>:void;// the actual function => (3)declarefunctionsearch<TextendsSearchArguments>(...args:T):ReturnSearch<T>;// z is voidconstz=search("omikron",(res)=>{});// y is Promise<unknown>consty=search("omikron");
以下是我们所做的:
Here’s what we did:
我们使用元组类型定义了参数列表。从 TypeScript 4.0 开始,我们可以像命名对象一样命名元组字段。我们创建一个联合,因为我们的函数签名有两种不同的变体。
We defined our argument list using tuple types. Since TypeScript 4.0, we can name tuple fields just like we would objects. We create a union because we have two different variants of our function signature.
typeReturnSearch根据参数列表变体选择返回类型。如果只是字符串,则返回Promise。否则返回void。
The ReturnSearch type selects the return type based on the argument list variant. If it’s just a string, return a Promise. Otherwise return void.
我们通过约束通用变量来添加我们的类型,SearchArguments以便我们可以正确选择返回类型。
We add our types by constraining a generic variable to SearchArguments so that we can correctly select the return type.
数量可不少!它还具有大量我们喜欢在 TypeScript 功能列表中看到的复杂功能:条件类型、泛型、泛型约束、元组类型、联合类型!我们获得了一些不错的自动完成功能,但它远不及简单函数重载的清晰度:
That is a lot! And it features a ton of complex features we love to see in TypeScript’s feature list: conditional types, generics, generic constraints, tuple types, union types! We get some nice autocomplete, but it’s nowhere near the clarity of a simple function overload:
functionsearch(query:string):Promise<unknown[]>;functionsearch(query:string,callback:(result:unknown[])=>void):void;// This is the implementation, it only concerns youfunctionsearch(query:string,callback?:(result:unknown[])=>void):void|Promise<unknown>{// Implement}
functionsearch(query:string):Promise<unknown[]>;functionsearch(query:string,callback:(result:unknown[])=>void):void;// This is the implementation, it only concerns youfunctionsearch(query:string,callback?:(result:unknown[])=>void):void|Promise<unknown>{// Implement}
我们只在实现部分使用联合类型。其余部分非常明确和清晰。我们知道我们的参数,也知道期望得到什么结果。没有繁文缛节,只有简单的类型。函数重载最好的部分是实际实现不会污染类型空间。你可以进行一轮any而不必在意。
We use a union type only for the implementation part. The rest is very explicit and clear. We know our arguments, and we know what to expect in return. No ceremony, just simple types. The best part of function overloads is that the actual implementation does not pollute the type space. You can go for a round of any and just not care.
函数重载可以使事情变得更容易的另一种情况是当您需要精确的参数及其映射时。让我们看一个将事件应用于事件处理程序的函数。例如,我们有一个MouseEvent并想MouseEventHandler用它来调用一个。键盘事件等也是如此。如果我们使用条件和联合类型来映射事件和处理程序,我们最终可能会得到这样的结果:
Another situation where function overloads can make things easier is when you need exact arguments and their mapping. Let’s look at a function that applies an event to an event handler. For example, we have a MouseEvent and want to call a MouseEventHandler with it. Same for keyboard events and so on. If we use conditionals and union types to map event and handler, we might end up with something like this:
// All the possible event handlerstypeHandler=|MouseEventHandler<HTMLButtonElement>|KeyboardEventHandler<HTMLButtonElement>;// Map Handler to EventtypeEv<T>=TextendsMouseEventHandler<inferR>?MouseEvent<R>:TextendsKeyboardEventHandler<inferR>?KeyboardEvent<R>:never;// Create afunctionapply<TextendsHandler>(handler:T,ev:Ev<T>):void{handler(evasany);// We need the assertion here}
// All the possible event handlerstypeHandler=|MouseEventHandler<HTMLButtonElement>|KeyboardEventHandler<HTMLButtonElement>;// Map Handler to EventtypeEv<T>=TextendsMouseEventHandler<inferR>?MouseEvent<R>:TextendsKeyboardEventHandler<inferR>?KeyboardEvent<R>:never;// Create afunctionapply<TextendsHandler>(handler:T,ev:Ev<T>):void{handler(evasany);// We need the assertion here}
乍一看,这看起来不错。但是,如果您考虑一下需要跟踪的所有变体,这可能会有点麻烦。
At first glance, this looks fine. It might be a bit cumbersome, though, if you think about all the variants you need to keep track of.
但是还有一个更大的问题。TypeScript 处理事件的所有可能变体的方式导致了意外交集,正如我们在12.6 节中看到的那样。这意味着,在函数主体中,TypeScript 无法判断你传递的是哪种处理程序。因此,它也无法判断我们得到的是哪种事件。因此,TypeScript 表示事件可以是鼠标事件和键盘事件。你需要传递可以处理两者的处理程序,而这并不是我们希望函数工作的方式。
But there’s a bigger problem. The way TypeScript deals with all possible variants of the event is causing an unexpected intersection, as we see in Recipe 12.6. This means that, in the function body, TypeScript can’t tell what kind of handler you are passing. Therefore, it also can’t tell which kind of event we’re getting. So TypeScript says the event can be both: a mouse event and a keyboard event. You need to pass handlers that can deal with both, which is not how we intend our function to work.
实际错误消息是“TS 2345:类型的参数KeyboardEvent<HTMLButtonElement> | MouseEvent<HTMLButtonElement, MouseEvent>不能分配给类型的参数MouseEvent<HTMLButtonElement, MouseEvent> & Keyboard Event<HTMLButtonElement>。”
The actual error message is “TS 2345: Argument of type KeyboardEvent<HTMLButtonElement> | MouseEvent<HTMLButtonElement, MouseEvent> is not assignable to parameter of type MouseEvent<HTMLButtonElement, MouseEvent> & Keyboard Event<HTMLButtonElement>.”
这就是为什么我们需要一个as any类型断言来使得能够实际调用带有事件的处理程序。
This is why we need an as any type assertion to make it possible to actually call the handler with the event.
函数签名在很多场景中都有效:
The function signature works in a lot of scenarios:
declareconstmouseHandler:MouseEventHandler<HTMLButtonElement>;declareconstmouseEv:MouseEvent<HTMLButtonElement>;declareconstkeyboardHandler:KeyboardEventHandler<HTMLButtonElement>;declareconstkeyboardEv:KeyboardEvent<HTMLButtonElement>;apply(mouseHandler,mouseEv);// worksapply(keyboardHandler,keyboardEv);// woirksapply(mouseHandler,keyboardEv);// breaks like it should!// ^// Argument of type 'KeyboardEvent<HTMLButtonElement>' is not assignable// to parameter of type 'MouseEvent<HTMLButtonElement, MouseEvent>'
declareconstmouseHandler:MouseEventHandler<HTMLButtonElement>;declareconstmouseEv:MouseEvent<HTMLButtonElement>;declareconstkeyboardHandler:KeyboardEventHandler<HTMLButtonElement>;declareconstkeyboardEv:KeyboardEvent<HTMLButtonElement>;apply(mouseHandler,mouseEv);// worksapply(keyboardHandler,keyboardEv);// woirksapply(mouseHandler,keyboardEv);// breaks like it should!// ^// Argument of type 'KeyboardEvent<HTMLButtonElement>' is not assignable// to parameter of type 'MouseEvent<HTMLButtonElement, MouseEvent>'
但一旦出现歧义,事情就不会按预期进行:
But once there’s ambiguity, things don’t work out as they should:
declareconstmouseOrKeyboardHandler:MouseEventHandler<HTMLButtonElement>|KeyboardEventHandler<HTMLButtonElement>;;// This is accepted but can cause problems!apply(mouseOrKeyboardHandler,mouseEv);
declareconstmouseOrKeyboardHandler:MouseEventHandler<HTMLButtonElement>|KeyboardEventHandler<HTMLButtonElement>;;// This is accepted but can cause problems!apply(mouseOrKeyboardHandler,mouseEv);
当是键盘处理程序时mouseOrKeyboardHandler,我们无法合理地传递鼠标事件。等等:这正是之前的 TS2345 错误试图告诉我们的!我们只是将问题转移到另一个地方,并用
as any断言使其保持沉默。
When mouseOrKeyboardHandler is a keyboard handler, we can’t reasonably pass a mouse event. Wait: this is exactly what the TS2345 error from before tried to tell us! We just shifted the problem to another place and made it silent with an
as any assertion.
明确、精确的函数签名使一切变得更容易。映射变得更清晰,类型签名更容易理解,并且不需要条件或联合:
Explicit, exact function signatures make everything easier. The mapping becomes clearer, the type signatures are easier to understand, and there’s no need for conditionals or unions:
// Overload 1: MouseEventHandler and MouseEventfunctionapply(handler:MouseEventHandler<HTMLButtonElement>,ev:MouseEvent<HTMLButtonElement>):void;// Overload 2: KeyboardEventHandler and KeyboardEventfunctionapply(handler:KeyboardEventHandler<HTMLButtonElement>,ev:KeyboardEvent<HTMLButtonElement>):void;// The implementation. Fall back to any. This is not a type!// TypeScript won't check for this line nor// will it show in the autocomplete.// This is just for you to implement your stuff.functionapply(handler:any,ev:any):void{handler(ev);}
// Overload 1: MouseEventHandler and MouseEventfunctionapply(handler:MouseEventHandler<HTMLButtonElement>,ev:MouseEvent<HTMLButtonElement>):void;// Overload 2: KeyboardEventHandler and KeyboardEventfunctionapply(handler:KeyboardEventHandler<HTMLButtonElement>,ev:KeyboardEvent<HTMLButtonElement>):void;// The implementation. Fall back to any. This is not a type!// TypeScript won't check for this line nor// will it show in the autocomplete.// This is just for you to implement your stuff.functionapply(handler:any,ev:any):void{handler(ev);}
函数重载可以帮助我们应对所有可能的情况。我们确保没有歧义类型:
Function overloads help us with all possible scenarios. We make sure there are no ambiguous types:
apply(mouseHandler,mouseEv);// works!apply(keyboardHandler,keyboardEv);// works!apply(mouseHandler,keyboardEv);// breaks like it should!// ^ No overload matches this call.apply(mouseOrKeyboardHandler,mouseEv);// breaks like it should// ^ No overload matches this call.
apply(mouseHandler,mouseEv);// works!apply(keyboardHandler,keyboardEv);// works!apply(mouseHandler,keyboardEv);// breaks like it should!// ^ No overload matches this call.apply(mouseOrKeyboardHandler,mouseEv);// breaks like it should// ^ No overload matches this call.
对于实现,我们甚至可以使用any。由于您可以确保不会遇到含糊不清的情况,因此您可以依靠随遇而安的类型,而不必担心。
For the implementation, we can even use any. Since you can make sure that you won’t run into a situation that implies ambiguity, you can rely on the happy-go-lucky type and don’t need to bother.
最后但并非最不重要的一点是条件类型和函数重载的组合。回想一下5.1 节中的示例:我们看到条件类型使函数体很难将值映射到相应的通用返回类型。将条件类型移动到函数重载并使用非常宽泛的函数签名进行实现,对函数的用户和实现者都有帮助 :
Last but not least, there’s the combination of conditional types and function overloads. Remember the example from Recipe 5.1: we saw that conditional types gave the function body a hard time to map values to the respective generic return types. Moving the conditional type to a function overload and using a very broad function signature for implementation helps both the users of the function as well as the implementers:
functioncreateLabel<Textendsnumber|string|StringLabel|NumberLabel>(input:T):GetLabel<T>;functioncreateLabel(input:number|string|StringLabel|NumberLabel):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}elseif(typeofinput==="string"){return{name:input};}elseif("id"ininput){return{id:input.id};}else{return{name:input.name};}}
functioncreateLabel<Textendsnumber|string|StringLabel|NumberLabel>(input:T):GetLabel<T>;functioncreateLabel(input:number|string|StringLabel|NumberLabel):NumberLabel|StringLabel{if(typeofinput==="number"){return{id:input};}elseif(typeofinput==="string"){return{name:input};}elseif("id"ininput){return{id:input.id};}else{return{name:input.name};}}
函数重载仍然非常有用,而且在很多情况下都是可行的方法。它们更易于阅读、更易于编写,而且在很多情况下,比我们用其他方法得到的更精确。
Function overloads are still very useful and, for a lot of scenarios, the way to go. They’re easier to read, easier to write, and, in a lot of cases, more exact than what we get with other means.
但这并不是非此即彼。如果你的场景需要,你可以随意混合搭配条件和函数重载。
But it’s not either-or. You can happily mix and match conditionals and function overloads if your scenario needs it.
遵循命名模式。
Follow a naming pattern.
TypeScript 的泛型可以说是该语言最强大的功能之一。它们为 TypeScript 自己的元编程语言打开了一扇大门,允许非常灵活和动态地生成类型。它几乎就是自己的函数式编程语言。
TypeScript’s generics are arguably one of the most powerful features of the language. They open a door to TypeScript’s own metaprogramming language, which allows for a very flexible and dynamic generation of types. It comes close to being its own functional programming language.
尤其是随着最新的 TypeScript 版本中字符串文字类型和递归条件类型的出现,我们可以设计出一些功能惊人的类型。第 12.2 节中的类型以 Express 风格解析路由信息并检索包含其所有参数的对象:
Especially with the arrival of string literal types and recursive conditional types in the most recent TypeScript versions, we can craft types that do astonishing things. This type from Recipe 12.2 parses Express-style from route information and retrieves an object with all its parameters:
typeParseRouteParameters<T>=Textends`${string}/:${inferU}/${inferR}`?{[PinU|keyofParseRouteParameters<`/${R}`>]:string}:Textends`${string}/:${inferU}`?{[PinU]:string}:{}typeX=ParseRouteParameters<"/api/:what/:is/notyou/:happening">// type X = {// what: string,// is: string,// happening: string,// }
typeParseRouteParameters<T>=Textends`${string}/:${inferU}/${inferR}`?{[PinU|keyofParseRouteParameters<`/${R}`>]:string}:Textends`${string}/:${inferU}`?{[PinU]:string}:{}typeX=ParseRouteParameters<"/api/:what/:is/notyou/:happening">// type X = {// what: string,// is: string,// happening: string,// }
当我们定义泛型类型时,我们也定义了泛型类型参数。它们可以是某种类型(或者更准确地说,是某种子类型):
When we define a generic type, we also define generic type parameters. They can be of a certain type (or more correctly, be a certain subtype):
typeFoo<Textendsstring>=...
typeFoo<Textendsstring>=...
它们可以有默认值:
They can have default values:
typeFoo<Textendsstring="hello">=...
typeFoo<Textendsstring="hello">=...
使用默认值时,顺序很重要。这只是与常规 JavaScript 函数的众多相似之处之一!既然我们几乎都在谈论函数,为什么我们要使用单字母名称作为泛型类型参数呢?
And when using default values, order is important. This is just one of many similarities to regular JavaScript functions! So since we are almost talking functions, why are we using single-letter names for generic type parameters?
大多数泛型类型参数以字母 开头T。后续参数按照字母顺序排列(U、V、W)或缩写,如Kfor key。然而,这可能会导致类型难以阅读。如果我看Extract<T, U>,很难分辨我们是T从 中提取的U,还是反过来。
Most generic type parameters start with the letter T. Subsequent parameters go along the alphabet (U, V, W) or are abbreviations like K for key. This can lead to highly unreadable types, however. If I look at Extract<T, U>, it is hard to tell if we extract T from U, or the other way around.
更详细一点会有所帮助:
Being a bit more elaborate helps:
typeExtract<From,Union>=...
typeExtract<From,Union>=...
现在我们知道我们想要从第一个参数中提取所有可赋值的内容Union。此外,我们知道我们想要一个联合类型。
Now we know that we want to extract from the first parameter everything that is assignable to Union. Furthermore, we understand that we want to have a union type.
类型是文档,我们的类型参数可以有可读的名称,就像您对常规函数所做的那样。采用命名方案,如下所示:
Types are documentation, and our type parameters can have speaking names, just like you would do with regular functions. Go for a naming scheme, like this one:
所有类型参数都以大写字母开头,就像您命名所有其他类型一样!
All type parameters start with an uppercase letter, like you would name all other types!
仅当用法完全清楚时才使用单个字母。例如,ParseRouteParams只能有一个参数,即路由。
Only use single letters if the usage is completely clear. For example, ParseRouteParams can have only one argument, the route.
不要缩写为T(这太……通用了!),而要缩写为能够明确我们正在处理的内容。例如,ParseRouteParams<R>,其中R代表Route。
Don’t abbreviate to T (that’s way too … generic!) but to something that clarifies what we are dealing with. For example, ParseRouteParams<R>, where R stands for Route.
很少使用单个字母;坚持使用短词或缩写:Elem对于Element,Route可以保持原样。
Rarely use single letters; stick to short words or abbreviations: Elem for Element, Route can stand as is.
使用前缀来区分内置类型。例如,Element已被使用,因此使用GElement(或坚持使用Elem)。
Use prefixes to differentiate from built-in types. For example, Element is taken, so use GElement (or stick with Elem).
使用前缀使通用名称更清晰:例如,
URLObj比更清晰。Obj
Use prefixes to make generic names clearer: URLObj is clearer than Obj,
for instance.
相同的模式适用于泛型类型内的推断类型。
Same patterns apply to inferred types within a generic type.
让我们再看ParseRouteParams一下,并更明确地说明我们的名字:
Let’s look at ParseRouteParams again and be more explicit with our names:
typeParseRouteParams<Route>=Routeextends`${string}/:${inferParam}/${inferRest}`?{[EntryinParam|keyofParseRouteParameters<`/${Rest}`>]:string}:Routeextends`${string}/:${inferParam}`?{[EntryinParam]:string}:{}
typeParseRouteParams<Route>=Routeextends`${string}/:${inferParam}/${inferRest}`?{[EntryinParam|keyofParseRouteParameters<`/${Rest}`>]:string}:Routeextends`${string}/:${inferParam}`?{[EntryinParam]:string}:{}
每种类型的意义变得更加清晰。我们还看到我们需要迭代Entry中的所有Param,即使Param只是一组单一类型。
It becomes a lot clearer what each type is meant to be. We also see that we need to iterate over all Entrys in Param, even if Param is just a set of one type.
可以说,它比以前更具可读性了!
Arguably, it’s a lot more readable than before!
有一个警告:几乎不可能区分类型参数和实际类型。还有另一种由Matt Pocock大力推广的方案:使用T前缀:
There is one caveat: it’s almost impossible to distinguish type parameters from actual types. There’s another scheme that has been heavily popularized by Matt Pocock: using a T prefix:
typeParseRouteParameters<TRoute>=Routeextends`${string}/:${inferTParam}/${inferTRest}`?{[TEntryinTParam|keyofParseRouteParameters<`/${TRest}`>]:string}:Routeextends`${string}/:${inferTParam}`?{[TEntryinTParam]:string}:{}
typeParseRouteParameters<TRoute>=Routeextends`${string}/:${inferTParam}/${inferTRest}`?{[TEntryinTParam|keyofParseRouteParameters<`/${TRest}`>]:string}:Routeextends`${string}/:${inferTParam}`?{[TEntryinTParam]:string}:{}
这接近于类型的匈牙利表示法。
This comes close to a Hungarian Notation for types.
无论您使用哪种变体,确保泛型类型对您和您的同事而言都是可读的,并且它们的参数不言自明,这与在其他编程语言中一样重要。
Whatever variation you use, making sure that generic types are readable to you and your colleagues, and that their parameters speak for themselves, is as important as in other programming languages.
将您的类型移至 TypeScript 游乐场并单独开发它们。
Move your types to the TypeScript playground and develop them in isolation.
图 12-1所示的TypeScript 操场是一个自 TypeScript 首次发布以来就一直存在的 Web 应用程序,展示了如何将 TypeScript 语法编译为 JavaScript。它的功能最初有限,专注于为新开发人员“破冰”,但近年来,它已成为在线开发的强大工具,功能丰富,是 TypeScript 开发不可或缺的一部分。TypeScript 团队要求人们提交问题,包括使用操场重新创建错误。他们还通过允许将夜间版本加载到应用程序中来测试新功能和即将推出的功能。简而言之:TypeScript 操场对于 TypeScript 开发至关重要。
The TypeScript playground as shown in Figure 12-1 is a web application that has been with TypeScript since its first release, showcasing how TypeScript syntax is compiled to JavaScript. Its capabilities were originally limited and focused on “breaking the ice” for new developers, but in recent years it has become a powerhouse of online development, rich in features and indispensable for TypeScript development. The TypeScript team asks people to submit issues including a re-creation of the bug using the playground. They also test new and upcoming features by allowing the nightly version to be loaded into the application. In short: the TypeScript playground is essential for TypeScript development.
对于您的常规开发实践,TypeScript 操场是独立于当前项目单独开发类型的好方法。随着 TypeScript 配置的增长,它们变得混乱,并且很难理解哪些类型对您的实际项目有贡献。如果您在类型中遇到奇怪或意外的行为,请尝试在操场中单独重新创建它们,而无需项目的其余部分。
For your regular development practices, the TypeScript playground is a great way to develop types in isolation, independent from your current project. As TypeScript configurations grow, they become confusing, and it becomes hard to understand which types contribute to your actual project. If you encounter weird or unexpected behavior in your types, try re-creating them in the playground, in isolation, without the rest of your project.
游乐场没有完整的tsconfig.json ,但你可以通过用户界面定义配置的重要部分,如图 12-2所示。或者,你可以直接在源代码中使用注释设置编译器标志:
The playground doesn’t feature a full tsconfig.json, but you can define the important pieces of your configuration via a user interface, as seen in Figure 12-2. Alternatively, you can set compiler flags using annotations directly in the source code:
// @strictPropertyInitialization: false// @target: esnext// @module: nodenext// @lib: es2015,dom
// @strictPropertyInitialization: false// @target: esnext// @module: nodenext// @lib: es2015,dom
虽然不太舒适,但非常符合人体工程学,因为它允许您更轻松地共享编译器标志。
Not as comfortable but highly ergonomic as it allows you to share compiler flags much more easily.
您还可以编译 TypeScript,获取提取的类型信息,运行小段代码以查看它们的行为方式,并将所有内容导出到各种目的地,包括其他流行的在线编辑器和 IDE。
You also can compile TypeScript, get extracted type information, run small pieces of code to see how they behave, and export everything to various destinations, including other popular online editors and IDEs.
您可以选择各种版本以确保您的错误不依赖于版本更新,并且您可以运行各种有据可查的示例,以便在尝试实际源代码的同时学习 TypeScript 的基础知识。
You can select various versions to ensure that your bug isn’t dependent on version updates, and you can run various, well-documented examples to learn the basics of TypeScript while trying out actual source code.
如范例 12.10中所述,如果不使用依赖项,JavaScript 开发将毫无意义。在 TypeScript 游乐场中,可以直接从 NPM 获取依赖项的类型信息。例如,如果您在 TypeScript 游乐场中导入 React,游乐场将尝试获取类型:
As noted in Recipe 12.10, developing JavaScript would be nothing without using dependencies. In the TypeScript playground, it’s possible to fetch type information for dependencies directly from NPM. If you import, for example, React within the TypeScript playground, the playground will try to acquire types:
首先,它将查看 NPM 上的相应包并检查其内容中是否有定义的类型或.d.ts文件。
First, it will look at the respective package on NPM and check if there are types defined or .d.ts files somewhere in its contents.
如果没有,它将在 NPM 上检查是否存在 Definitely Typed 类型信息,并下载相应的@types包。
If not, it will check on NPM if Definitely Typed type information exists and will download the respective @types package.
这是递归的,这意味着如果某些类型需要来自其他包的类型,则类型获取也将通过类型依赖关系进行。对于某些包,您甚至可以定义要加载哪个版本:
This is recursive, meaning that if some types require types from other packages, type acquisition will also go through the type dependencies. For some packages, you can even define which version to load:
import{render}from"preact";// types: legacy
import{render}from"preact";// types: legacy
这里,types设置为legacy,从 NPM 加载相应的旧版本。
Here, types is set to legacy, which loads the respective legacy version from NPM.
生态系统还有更多内容。TypeScript 游乐场的一个重要工具是Twoslash。Twoslash 是 TypeScript 文件的一种标记格式,可让您突出显示代码、处理多个文件以及显示 TypeScript 编译器创建的文件。它非常适合博客和网站 - 您基本上有一个用于代码示例的内联 TypeScript 编译器 - 但如果您需要创建复杂的调试 场景,它也很棒。
There’s more to the ecosystem. An important tool of the TypeScript playground is Twoslash. Twoslash is a markup format for TypeScript files that lets you highlight code, handle multiple files, and show the files the TypeScript compiler creates. It’s fantastic for blogs and websites—you basically have an inline TypeScript compiler for code examples—but it’s also fantastic if you need to create complex debugging scenarios.
编译器标志注释由 Twoslash 处理,但您也可以通过在变量名称下直接添加注释中的标记来获取有关当前类型的内联提示:
The compiler flag annotations are handled by Twoslash, but you can also get inline hints on current types by adding a marker in a comment directly under a variable name:
// @jsxFactory: himport{render,h}from"preact";functionHeading(){return<h1>Hello</h1>}constelem=<Heading/>// ^?// This line above triggers inline hints
// @jsxFactory: himport{render,h}from"preact";functionHeading(){return<h1>Hello</h1>}constelem=<Heading/>// ^?// This line above triggers inline hints
您可以在图 12-3中看到结果。
You can see the result in Figure 12-3.
Twoslash 也是bug workbench的一部分,它是 Playground 的一个分支,专注于创建和显示 bug 的复杂再现。在这里,您还可以定义多个文件来查看导入和导出的工作原理:
Twoslash is also part of the bug workbench, which is a fork of the playground with an emphasis on creating and displaying complex reproductions of bugs. Here, you can also define multiple files to see how imports and exports work:
exportconsta=2;// @filename: a.tsimport{a}from"./input.js"console.log(a);
exportconsta=2;// @filename: a.tsimport{a}from"./input.js"console.log(a);
第一个注释会触发多文件支持。此行之前的所有内容都会变成一个名为input.tsx@filename的文件,基本上就是您的主入口点。
Multifile support is triggered by the first @filename annotation. Everything before this line becomes a file called input.tsx, basically your main entry point.
最后但并非最不重要的一点是,该游乐场可以作为研讨会和培训的整个演示套件。使用 Twoslash,您可以在 GitHub Gist 存储库中创建多个文件,并将 TypeScript 文件与文档一起加载为 Gist 文档集的一部分,如图12-4所示。
Last but not least, the playground can work as your entire demo suite for workshops and trainings. Using Twoslash, you can create multiple files in a GitHub Gist repository and load the TypeScript files along with documentation as part of a Gist docset, as seen in Figure 12-4.
这对于沉浸式学习非常有用。从简单的复制品到成熟的演示套件,TypeScript 游乐场是 TypeScript 开发人员的一站式资源——无论您是需要提交错误、尝试新内容还是单独处理类型。它是一个很好的入门资源,从那里您可以轻松迁移到“真正的” IDE 和工具。
This is immensely powerful for immersive learning. From mere reproductions to full-fledged demo suites, the TypeScript playground is the one-stop source for TypeScript developers—whether you need to file bugs, try out something new, or work on types in isolation. It’s a great resource to start with, and from there you can easily migrate to “real” IDEs and tools.
使用引用三斜杠指令以及模块、命名空间和接口进行声明合并。
Use reference triple-slash directives, as well as modules, namespaces, and interfaces for declaration merging.
如果没有外部库帮你处理大量工作,编程将会非常困难。JavaScript 的生态系统可以说是第三方依赖项最丰富的生态系统之一,主要通过NPM实现。此外,它们中的大多数都支持 TypeScript,无论是通过内置类型还是通过 Definitely Typed 的类型。据 TypeScript 团队称,几乎80% 的 NPM 都是 typed。然而,仍然有一些奇怪的坚持:例如,库不是用 TypeScript 编写的,或者您自己公司的遗留代码,您仍然需要使其与当今的软件兼容。
Programming would be tough without external libraries that take care of a lot of work for you. JavaScript’s ecosystem is arguably one of the richest when it comes to third-party dependencies, mainly through NPM. Also, most of them come with TypeScript support, either through built-in types or through types from Definitely Typed. According to the TypeScript team, almost 80% of NPM is typed. However, there is still the odd holdout: for example, libraries are not written in TypeScript, or legacy code from your own company that you still need to make compatible with today’s software.
想象一下名为“lib”的库,它公开了一个Connector可用于定位内部系统的类。此库有多个版本,并且不断添加功能:
Think of a library called “lib”, which exposes a Connector class that you can use to target internal systems. This library exists in multiple versions, and features have been added constantly:
import{Connector}from"lib";// This exists in version 1constconnector=newConnector();constconnection=connector.connect("127.0.0.1:4000");connection.send("Hi!");// This exists in version 2connection.close();
import{Connector}from"lib";// This exists in version 1constconnector=newConnector();constconnection=connector.connect("127.0.0.1:4000");connection.send("Hi!");// This exists in version 2connection.close();
值得注意的是,您组织内的多个项目可以使用此库,且版本各异。您的任务是编写类型,以便您的团队获得正确的自动完成和类型信息。
It’s worth noting that this library can be used by multiple projects within your organization, with varying versions. Your task is to write types so your teams get proper autocomplete and type information.
在 TypeScript 中,您可以通过为每个版本的库创建环境模块声明来提供库类型的多个版本。环境模块声明是一个带有.d.ts扩展名的文件,它为 TypeScript 提供未用 TypeScript 编写的库的类型。
In TypeScript, you can provide multiple versions of a library’s types by creating an ambient module declaration for each version of the library. An ambient module declaration is a file with a .d.ts extension that provides TypeScript with the types for a library not written in TypeScript.
默认情况下,TypeScript 是贪婪的:它包含类型定义并尽可能地匹配所有内容。如果要限制 TypeScript 的文件访问权限,请确保使用tsconfig.json中的"exclude"和属性:"include"
By default, TypeScript is greedy: it includes type definitions and globs everything it can. If you want to limit TypeScript’s file access, make sure to use the "exclude" and "include" properties in tsconfig.json:
{"compilerOptions":{// ..."typeRoots":["@types"],"rootDir":"./src","outDir":"dist",},"include":["./src","./@types"]}
{"compilerOptions":{// ..."typeRoots":["@types"],"rootDir":"./src","outDir":"dist",},"include":["./src","./@types"]}
我们在tsconfig.json中包含的文件夹旁边创建一个文件夹。在这里,我们创建一个名为lib.v1.d.ts的文件,其中存储有关如何创建对象的基本信息:
We create a folder next to the folders we included in tsconfig.json. Here, we create a file called lib.v1.d.ts, where we store the basic information on how objects are created:
declaremodule"lib"{exportinterfaceConnectorConstructor{new():Connector;}varConnector:ConnectorConstructor;exportinterfaceConnector{connect(stream:string):Connection;}exportinterfaceConnection{send(msg:string):Connection;}}
declaremodule"lib"{exportinterfaceConnectorConstructor{new():Connector;}varConnector:ConnectorConstructor;exportinterfaceConnector{connect(stream:string):Connection;}exportinterfaceConnection{send(msg:string):Connection;}}
请注意,我们使用模块来定义模块的名称,并且我们还对大多数类型使用接口。模块和接口都可以进行声明合并,这意味着我们可以在不同的文件中增加新类型,然后 TypeScript 将它们合并在一起。如果我们想要定义多个版本,这一点至关重要。
Note that we use modules to define the name of the module and that we also use interfaces for most of our types. Both modules and interfaces are open to declaration merging, which means we can add new types in different files and TypeScript merges them together. This is crucial if we want to define multiple versions.
还请注意,我们使用了构造函数接口模式(参见11.3节)
Connector:
Also note that we use the constructor interface pattern (see Recipe 11.3) for
Connector:
exportinterfaceConnectorConstructor{new():Connector;}varConnector:ConnectorConstructor;
exportinterfaceConnectorConstructor{new():Connector;}varConnector:ConnectorConstructor;
通过这样做,我们可以改变构造函数的签名并确保 TypeScript 可以识别可实例化的类。
In doing so, we can change the signature of the constructor and make sure that an instantiable class is being recognized by TypeScript.
在另一个名为lib.v2.d.ts的文件中,在lib.v1.d.ts旁边,我们重新声明"lib"并添加了更多方法Connection。通过声明合并,close方法被添加到Connection接口中:
In another file called lib.v2.d.ts, next to lib.v1.d.ts, we redeclare "lib" and add more methods to Connection. Through declaration merging, the close method gets added to the Connection interface:
/// <reference path="lib.v1.d.ts" />declaremodule"lib"{exportinterfaceConnection{close():void;}}
/// <reference path="lib.v1.d.ts" />declaremodule"lib"{exportinterfaceConnection{close():void;}}
使用三斜杠指令,我们从lib.v2.d.ts引用lib.v1.d.ts,表示版本 1 中的所有内容都将包含在版本 2 中。
Using triple-slash directives, we refer from lib.v2.d.ts to lib.v1.d.ts, signaling that everything from version 1 is to be included in version 2.
所有这些文件都位于名为@lib的文件夹中。使用我们之前声明的配置,TypeScript 不会选择它们。但是,我们可以编写一个新文件lib.d.ts并将其放在@types中,然后从那里引用我们想要包含的版本:
All those files exist in a folder called @lib. Using the configuration we declared earlier, TypeScript won’t pick them up. We can, however, write a new file lib.d.ts and put it in @types, and from there, refer to the version we want to include:
/// <reference path="../@lib/lib.v2.d.ts" />declaremodule"lib"{}
/// <reference path="../@lib/lib.v2.d.ts" />declaremodule"lib"{}
从“../@lib/lib.v2.d.ts”到“../@lib/lib.v1.d.ts”的简单更改将会改变我们针对的版本,同时我们仍然独立维护所有库版本。
A simple change from “../@lib/lib.v2.d.ts” to “../@lib/lib.v1.d.ts” will change the version we target, while we still maintain all library versions independently.
如果你很好奇,可以尝试查看 TypeScript 中包含的库文件。它们是外部类型定义的宝库,有很多东西需要学习。如果你使用编辑器查找引用,例如,对Object.keys,你会看到此函数存在于多个位置,并且根据你的 TypeScript 配置,将包含正确的文件。图 12-5显示了 Visual Studio Code 如何显示 的各种文件位置Object.keys。TypeScript 非常灵活,你可以在项目中使用相同的技术,甚至可以扩展 TypeScript 的内置类型本身(参见范例 9.7)。
If you are curious, try looking into the included library files from TypeScript. They are a treasure trove of external type definitions, and there is a lot to learn. If you use your editor to find references, for example, to Object.keys, you will see that this function exists in multiple locations, and based on your TypeScript configuration, the right file will be included. Figure 12-5 shows how Visual Studio Code displays various file locations for Object.keys. TypeScript is so flexible that you can use the same techniques for your project, even extending TypeScript’s built-in types themselves (see Recipe 9.7).
总之,在 TypeScript 中提供库类型的多个版本可以通过为每个版本的库创建环境模块声明并在 TypeScript 代码中引用适当的声明来实现。希望您能够在项目中使用包管理器来管理不同版本的库及其相应的类型,从而更轻松地处理依赖关系并避免冲突。
In conclusion, providing multiple versions of a library’s types in TypeScript can be done by creating ambient module declarations for each version of the library and referencing the appropriate declaration in your TypeScript code. Hopefully, you will be able to use package managers in your project to manage different versions of libraries and their corresponding types, making it easier to handle dependencies and avoid conflicts.
不要编写复杂且繁琐的类型。TypeScript 是渐进式的;使用可以提高效率的方法。
Don’t write elaborate and complicated types. TypeScript is gradual; use what makes you productive.
我想用一些关于如何在适当的时候停止的一般建议来结束这本书。如果你读完了整本书并读到这里,那么你已经阅读了超过一百个关于日常 TypeScript 问题的建议。无论是项目设置、需要找到正确类型的复杂情况,还是当 TypeScript 遇到过于严格的情况时的解决方法,我们都已经涵盖了。
I want to end this book with some general advice on how to stop at the right time. If you have read through the entire book and ended up here, you have read through more than one hundred recipes with a lot of advice about everyday TypeScript problems. Be it project setup, complicated situations where you need to find the right type, or workarounds when TypeScript runs into a situation where it’s too strict for its own good, we have covered it all.
解决方案可能会变得非常复杂,尤其是当我们进入条件类型及其周围的一切领域时,例如辅助类型、可变元组类型和字符串模板文字类型。TypeScript 的类型系统无疑是强大的,特别是如果您了解每个决策、每个功能都源于 JavaScript 是这一切的根源。为一种天生动态的编程语言创建一个提供强大静态类型的类型系统是一项了不起的成就。我对雷德蒙德的聪明才智深表钦佩,是他们让这一切成为可能。
Solutions can get very complex, especially when we enter the area of conditional types and everything around them, like helper types, variadic tuple types, and string template literal types. TypeScript’s type system is undoubtedly powerful, especially if you understand that every decision, every feature, has its roots in the fact that JavaScript lies underneath it all. Creating a type system that gives you strong, static types for a programming language that is so inherently dynamic is an amazing achievement. I have nothing but the deepest admiration for the bright minds in Redmond who made all of this possible.
然而,不可否认的是,事情有时会变得非常复杂。类型可能难以阅读或创建,而类型系统本身就是图灵完备的元编程系统,需要测试库,这一事实也无济于事。开发人员以了解其技术和工具的各个方面而自豪,他们通常更喜欢复杂的类型解决方案,而不是简单的类型,虽然这些类型不能提供相同的类型安全性,但最终更易于阅读和理解。
However, undeniably, things can get very complicated at times. Types can be hard to read or create, and the fact that the type system is its own Turing-complete meta-programming system that needs testing libraries doesn’t help. And developers take pride in understanding every aspect of their craft and tools, often preferring a complex type solution over simpler types that don’t give the same type safety but are ultimately easier to read and understand.
一个深入研究类型系统细节的项目叫做“类型挑战”。这是一个非常棒的脑筋急转弯项目,它展示了类型系统的可能性。我摆弄了一些更具挑战性的谜语,得到了如何更好地解释类型系统的好主意。虽然谜题对于训练开发人员的思维非常有用,但大多数谜题都缺乏对现实世界日常情况的充分了解。
A project that goes into the nitty-gritty of the type system is called Type Challenges. It’s a fantastic project of brainteasers that show what’s possible with the type system. I fiddle around with some of the more challenging riddles, getting great ideas for how to explain the type system better. And while puzzles are fantastic for training a developer’s mind, most of them lack a significant grasp of real-world, everyday situations.
在这些情况下,我们常常会忽略 TypeScript 的出色功能,而这在主流编程语言中并不常见:它对类型的逐步采用。诸如any泛型类型参数和类型断言之类的工具,以及只需几句注释就能编写简单的 JavaScript 的事实,使进入的门槛大大降低。TypeScript 团队和 TC39 的最新努力是通过向 JavaScript 添加类型注释来进一步降低门槛,这是一项正在讨论的提案。该提案的目标不是使 JavaScript 类型安全,而是如果我们想要拥有简单易懂的类型注释,则删除编译步骤。JavaScript 引擎可以将它们视为注释,类型检查器可以获得有关程序语义的真实信息。
And those are the situations where we often overlook TypeScript’s wonderful capability that you don’t often see in mainstream programming languages: its gradual adoption of types. Tools like any, generic type parameters, and type assertions and the fact that you can write simple JavaScript with a couple of comments make the barrier to entry so much lower. The latest effort from the TypeScript team and TC39 is to lower the barrier even more by adding type annotations to JavaScript, a proposal currently in discussion. The goal of this proposal is not to make JavaScript type safe but to remove compile steps if we want to have simple, easy-to-understand type annotations. JavaScript engines can treat them as comments, and type-checkers can get real information on the program’s semantics.
作为开发人员、项目负责人、工程师和架构师,我们应该使用此功能。简单类型始终是更好的类型:更易于理解且更易于使用。
As developers, project leaders, engineers, and architects, we should use this feature. Simple types are always better types: easier to understand and much easier to consume.
TypeScript网站将其声明从“可扩展的 JavaScript”更改为“具有类型语法的 JavaScript”,这应该可以让您了解如何在项目中使用 TypeScript:编写 JavaScript,在必要时进行注释,编写简单但全面的类型,并使用 TypeScript 作为记录、理解和传达您的软件的一种方式。
The TypeScript website changed its claim from “JavaScript that scales” to “JavaScript with syntax for types,” which should give you an idea of how to approach TypeScript in projects: write JavaScript, annotate where necessary, write simple but comprehensive types, and use TypeScript as a way to document, understand, and communicate your software.
我认为 TypeScript 遵循帕累托原则:80% 的类型安全来自其 20% 的功能。这并不意味着其余的功能不好或不必要。我们只是用了一百个配方来了解我们真正需要 TypeScript 更高级功能的情况。它应该只是让你知道在哪里投入精力。不要每次都遇到高级 TypeScript 诡计。监控失败类型是否存在问题。估计在程序中更改类型的工作量,并做出明智的决定。还要知道,在改进过程中(参见配方 12.2),多个步骤的原因是为了能够轻松停止。
I think TypeScript follows the Pareto principle: 80% of type safety comes from 20% of its features. This doesn’t mean the rest of it is bad or unnecessary. We just spent one hundred recipes to understand situations where we effectively need TypeScript’s more advanced features. It should just give you an idea of where to put effort. Don’t run into advanced TypeScript trickery on every occasion. Monitor if loser types are a problem. Estimate the effort to change types in your program, and make well-informed decisions. Also know that in a refinement process (see Recipe 12.2), the reason for multiple steps is to easily be able to stop.
TypeScript Cookbook封面上的动物是一只梅头鹦鹉(Psittacula cyanocephala)。这些鸟是印度次大陆的特有物种。它们也常被当作宠物饲养。与其他作为宠物饲养的鹦鹉一样,梅头鹦鹉需要定期互动和社交。与其他鹦鹉相比,它们攻击性和占有欲较低,被认为是温和、善于交际和深情的。
The animal on the cover of TypeScript Cookbook is a plum-headed parakeet (Psittacula cyanocephala). These birds are endemic to the Indian subcontinent. They are also commonly kept as pets. Like other parrots kept as pets, plum-headed parakeets require regular interaction and socialization. Compared to other parrots, they are less aggressive and possessive and are considered to be gentle, social, and affectionate.
梅头鹦鹉具有二态性,这意味着雄性和雌性的特征很容易区分。它们的身体都以绿色为主,胸部、腹部、背部和翅膀上有各种不同的颜色。雄性的头部呈紫红色,脖子周围有黑色项圈。雌性的头部呈蓝灰色,脖子周围有黄色羽毛。它们是中型鸟类,长约 12 英寸,体重在 2.3 至 2.8 盎司之间。梅头鹦鹉的平均寿命为 15 至 20 年。
Plum-headed parakeets are dimorphic, which means that males and females have easily distinguishable features. Both have predominantly green bodies with a variety of different shades on their breast, abdomen, back, and wings. Males have a purplish-red colored head outlined with a black collar around the neck. Females have bluish-gray heads and yellow-tinged feathers around their necks. They are medium-sized birds that are approximately 12 inches long and weigh between 2.3 to 2.8 ounces. An average lifespan for plum-headed parakeets is between 15 to 20 years.
野生长尾小鹦鹉的典型饮食包括水果、种子、肉质花瓣和谷物。它们还会袭击农田和果园。在圈养环境中,它们最健康的饮食是喂食高质量的种子和颗粒混合物,并辅以新鲜水果和蔬菜(例如,豆芽、绿叶蔬菜、浆果和辣椒)。
A typical diet for these parakeets in the wild includes fruits, seeds, fleshy flower petals, and grains. They have also been known to raid agricultural fields and orchards. In captivity, they are healthiest when fed high-quality seed and pellet mixes that are supplemented with fresh fruits and vegetables (e.g., sprouts, leafy greens, berries, and peppers).
这些鸟通常栖息在从喜马拉雅山脚向南到斯里兰卡的林地和森林地区,包括印度、巴基斯坦和孟加拉国。虽然由于栖息地丧失,紫头鹦鹉的数量逐渐减少,但它们并没有濒临灭绝。奥莱利封面上的许多动物都濒临灭绝;它们对世界都很重要。
These birds typically populate woodlands and forested areas from the foothills of the Himalayas south to Sri Lanka, including India, Pakistan, and Bangladesh. While there has been a gradual decline in numbers due to habitat loss, plum-headed parakeets are not in danger of extinction. Many of the animals on O’Reilly covers are endangered; all of them are important to the world.
封面插图由 Karen Montgomery 绘制,基于Histoire Naturelle的黑白版画。封面字体为 Gilroy Semibold 和 Guardian Sans。文本字体为 Adobe Minion Pro;标题字体为 Adobe Myriad Condensed;代码字体为 Dalton Maag 的 Ubuntu Mono。
The cover illustration is by Karen Montgomery, based on a black-and-white engraving from Histoire Naturelle. The cover fonts are Gilroy Semibold and Guardian Sans. The text font is Adobe Minion Pro; the heading font is Adobe Myriad Condensed; and the code font is Dalton Maag’s Ubuntu Mono.